Repository: dankamongmen/notcurses (language: python)

Issue:
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.
add wezterm detection/differentiation
The wezterm FAQ explicitly disclaims any independent terminfo database, stating that `xterm-256color` ought be used. OK, that's fine. For advanced features beyond terminfo, however (see e.g. #1715 and #1758), we need be able to differentiate wezterm from the forty thousand other terminals claiming to be xterm.

I don't see any way to interrogate wezterm, but according to https://github.com/wez/wezterm/issues/230 we can look for the `TERM_PROGRAM environment variable, and indeed:

```
declare -x TERM_PROGRAM="WezTerm"
declare -x TERM_PROGRAM_VERSION="20210610-204057-2e7dc70e"
```

alright, i guess we're using that.
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.
handle terminal interrogation failure better
Currently, if we interrogate a terminal, and don't get a response, the program will simply hang. Hence the requirement that the user explicitly call some function (e.g. `notcurses_check_pixel_support()`) before we begin interrogation.

I hate this so fucking much. I have no idea whether user input can break up the terminal response, but it wouldn't surprise me at all (and if it doesn't break the response, we'll at best lose said input). Hate hate hate hate hate.

I've been unwilling to put in a timeout, because on a loaded system, a slow reply is totally normal, and what timeout would we use, and even worse would be pushing that timeout question to the user, because they know nothing. I stand by this decision, because I am a dinosaur and a curmudgeon and everyone's too busy these days. Let the computer cogitate, if that's what it needs to do.

I do think, however, that we need at least print a diagnostic after a certain time goes by without a response, so the user isn't just left with no idea what's up. I think this can be dumped to stderr; it'll be jarring and disrupt any drawn screen, but hey, you've been sitting around for five (or whatver) stimulus-free seconds already, and in all likelihood you'll be sitting there until time ends or the power fails.

So yeah, make a concession to reality, and after say 5 seconds without a response from the terminal, print a diagnostic to stderr about "N seconds without a response from the terminal....". Maybe include the `TERM` value in this output for the inevitable bug reports.

Better yet would be a method to break user input up from terminal responses, but I doubt that this is possible =[. Maybe we ought take unrecognized input and queue it up as user input, but then we're just forwards-incompatible with new, wacky returns from tomorrow's terminals.

Fuck the world, save yourself.


Discussion / hints:
i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.

i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.
i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.
i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.
i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.
i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.
i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.
i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.
i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.
i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.
i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.
i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.
i think assuming that the terminal communicates back to us via escape codes is reasonable. so if we're not in an escape code, punt that over to the general input queue. this ought take care of e.g. input piped in.
note that in #1506 we stopped attempting terminal interrogation when `TIOCGSIZE` doesn't return pixel information, which has proven a pretty good heuristic thus far.
can't we just open `/dev/tty` as a new file descriptor and talk to that directly? wouldn't that at least fix the piped-input case?
> can't we just open /dev/tty as a new file descriptor and talk to that directly?

I'm not familiar enough with notcurses to say if this is a problem or not, but unless you are **sure** stdin/stdout is a tty you **need** to open, and use `/dev/tty`. Of course, it will still be intermingled with user input, if stdin **is** the tty.

> i think assuming that the terminal communicates back to us via escape codes is reasonable.

FWIW, I'm not aware of any query replies that aren't escapes. In fact, I'm not aware of _anything_, besides user input, that isn't escapes. Foot never sends anything that isn't an escape, except for user input.

A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.
> A common trick to avoid waiting forever for replies to queries that aren't supported on all terminals is to send another query right after the first one. The last one should be one that you are sure (as sure as one can be) is supported on all terminals (_Send Device Attributes_, `\E[c`, is a common choice). This way, you'll get at least _something_ returned. You'll have to parse the response to determine if you got one, or two replies.

if there is a truly general such code, this is a very elegant way to handle the problem; i love it! <3 <3 <3 <3 <3 <3
> if there is a truly general such code

Let me put it this way; I'm not aware of any terminals that do **not** support `\E[c`.
so i have one last concern before implementing this sweet technique: how does this interact with someone redirecting stdin? the particular case i'm worrying about is when a large source is sent to stdin (someone holding down on a key might presumably have the same result, read on).

even if i don't have an internal thread reading off input, and instead am just checking input when either (a) i've just written something expecting a result, or (b) the user requests it, let's say there's a 2GB file being sent to stdin. i would need to buffer all of that in the (a) case, since the user hasn't asked for it yet, and it's going to come before my expected answer from the terminal.

of course, if stdin is redirected to a file, it presumably is not getting input from the tty like normal. so perhaps i can read responses exclusively on the actual terminal fd (i.e. the one to which i wrote the query), and it ought never see redirected stdin. regular stdin (i.e. from a tty) would still need to be buffered in this case, and indeed, it might be broken across both stdin and the termfd...we'll need experiment with that.
and this is of course predicated on successful implementation of #1692, which i intend to handle very soon.
You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

But, you're right, if stdin is **not** re-directed, there's a chance you'll be reading user input before the reply arrives.

Fun fact: the terminal itself has more or less the same problem when pasting text from the clipboard. And yes, in this case, foot buffers everything else until the paste is complete, then it drains the buffered writes.

So, it's very unlikely you'll get 2GB user input on the TTY. A re-directed stdin, yes, but you're not reading TTY replies from that. However, you can get fairly large amounts of data if the user pastes a large amount of text.

If stdin is not re-directed, then it and your term FD refers to the same underlying TTY FD. And, the stdin `FILE *` stream may be buffered by the OS already. Not sure how you've set things up? So you absolutely need to be careful how you read from them.
it would be good to go ahead and implement this ASAP, since it'll allow us to run on both mainbranch and graphicsbranch alacritty.
> You should be reading terminal query responses directly from the TTY, i.e. not stdin. Simply because you'll never get any replies if you read from a re-directed stdin.

I'm never reading from stdin; i'm worried about when it's not redirected, user input being interpreted as terminal responses. but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any `ESCDELAY` business), they ought be distinguishable. i think we're good to go here!
> but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable

I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.
> > but so long as terminal responses are always escaped, and arrive atomically (not safe to assume with old terminals, but hopefully safe now--i'm not getting into any ESCDELAY business), they ought be distinguishable
> 
> I don't think you can assume that. You can probably assume that a single ESC is just that, but not that you'll have the entire response atomically. I _think_ it's theoretically possible to get a single ESC in one read, even though it's part of a longer response.

ugh, that leads to the whole `ESCDELAY` situation that NCURSES has -- how long do you wait, following receipt of an \e, before handing it all off to the user as regular input? 

so far as i can tell, options include

* not allowing the user to make use of escape (unacceptable)
* some timeout (unacceptable, with more steps)
* assuming atomic receipt of escape sequences

there are some guarantees of atomicity of sufficiently small `write(2)`s to a pipe, but i don't think they apply to this case, and i'd be hesitant to rely on them anyway. obviously there's no hope of atomicity if the reply is written across multiple `write()`s, but i'd hope that doesn't happen. what are your thoughts?
i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.
> i'm thinking that, rather than send DA and then conditionally send 2x(XTSMGRAPHICS+DA), we ought just send 2xXTSMGRAPHICS+DA at the beginning. it cuts two round-trip times and several bytes, we need the same parsing logic either way (pump that lemma!), and it makes our behavior more deterministic. furthermore, i'd say we probably ought toss this out immediately upon starting, so the reply is ideally available by the time we've initialized, minimizing latency to quiescence.

shit, if we assume we're always going to get a reply from DA, we can eliminate the entire `notcurses_check_pixel_support()` fecaoma, which would make me truly happy. ok, let's go for it!
so to do this we'd just make sixel support conditional on successful parse of the second XTSMGRAPHICS, or better yet both of them. in that case, there's no need to trigger it off any particular DA response. yes, i love this!
I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the _theoretical_ problem lies.

> obviously there's no hope of atomicity if the reply is written across multiple write()s, but i'd hope that doesn't happen

I do think that may happen. Not sure how common. In foot, query replies are usually send in a single `write()`, but not always. For example, OSC-52 (clipboard data) is split up. `DA` and `XTGRAPHICS` responses are sent in single writes. But more importantly, alt-prefixed user input is split up, so that the ESC is sent first, and the character next.

I don't mind updating foot, but I think that if one terminal send split up responses, then others are too.
> I think that any write, regardless how small, may be split up if the pipe is already almost full? That's where I think the theoretical problem lies.

absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs. i agree there are fundamental races here, but there seem to be at **any** selection of timeout (i'm suggesting a choice of 0).
> absolutely, but if i throw this off immediately (and i only need it at startup, though that's not true for cursor location queries), it "ought" be among my first inputs

Agreed, I don't think this will be a problem.

> ugh, that leads to the whole ESCDELAY situation that NCURSES has

FWIW, foot has a feature to enable a custom escape for <kbd>Escape</kbd>, for this exact reason. Not suggesting you use that, because a) it's limited to foot, and b) we're likely to implement Kitty's new keyboard protocol which should make our custom escape obsolete.

Fix patch:
diff --git a/NEWS.md b/NEWS.md
index cb65d27996..f6666b83b1 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -1,6 +1,19 @@
 This document attempts to list user-visible changes and any major internal
 rearrangements of Notcurses.
 
+* 2.3.5 (not yet released)
+  * Happy day! The terminal interrogation routines in the initialization code 
+    have been completely revamped. The first outcome of this is that Sixel
+    parameters are now opportunistically read at startup, and thus there is
+    no longer any need to call `notcurses_check_pixel_support()` before
+    using `NCBLIT_PIXEL`. If it's there, it'll be used; if not, it'll degrade
+    or fail. Indeed, this function will likely be removed for ABI3. The new
+    routines rely on the terminal answering the Send Device Attributes escape;
+    if it does not, Notcurses may refuse to start, or even hang. Please report
+    a bug if you run into this.
+  * `CELL_ALPHA_*` macros are now `NCALPHA_*`. The former will remain
+    `#define`d until ABI3.
+
 * 2.3.4 (2021-06-12)
   * Added the flag `NCVISUAL_OPTION_NOINTERPOLATE` to use non-interpolative
     scaling in `ncvisual_render()`. `ncvisual_render()` without a multimedia
diff --git a/README.md b/README.md
index 6410f459d9..002d19206d 100644
--- a/README.md
+++ b/README.md
@@ -264,7 +264,7 @@ to breaking under incorrect `TERM` values. If you're not using `xterm`, your
 
 * **Q:** How about *arbitrary image manipulation here* functionality? **A:** I'm not going to beat ImageMagick et al. on image manipulation, but you can load an `ncvisual` from RGBA memory using `ncvisual_from_rgba()`.
 
-* **Q:** My program locks up when I run `notcurses_check_pixel_support()`. **A**: Detecting bitmap support requires interrogating the terminal. If the terminal doesn't reply to standard interrogations, send upstream a patch, or use a different terminal. Generally, only old and unsupported terminal emulators exhibit this behavior.
+* **Q:** My program locks up during initialization. **A**: Notcurses interrogates the terminal. If the terminal doesn't reply to standard interrogations, file a Notcurses bug, send upstream a patch, or use a different terminal. Generally, only old and unsupported terminal emulators exhibit this behavior.
 
 * **Q:** How do I mix Rendered and Direct mode? **A:** You really don't want to. You can stream a subprocess to a plane with the `ncsubproc` widget.
 
diff --git a/USAGE.md b/USAGE.md
index 59d01ea98a..a3bf94d20e 100644
--- a/USAGE.md
+++ b/USAGE.md
@@ -942,10 +942,8 @@ ncplane_dim_x(const struct ncplane* n){
 
 // Retrieve pixel geometry for the display region ('pxy', 'pxx'), each cell
 // ('celldimy', 'celldimx'), and the maximum displayable bitmap ('maxbmapy',
-// 'maxbmapx'). Note that this will call notcurses_check_pixel_support(),
-// possibly leading to an interrogation of the terminal. If bitmaps are not
-// supported, 'maxbmapy' and 'maxbmapx' will be 0. Any of the geometry
-// arguments may be NULL.
+// 'maxbmapx'). If bitmaps are not supported, 'maxbmapy' and 'maxbmapx' will
+// be 0. Any of the geometry arguments may be NULL.
 void ncplane_pixelgeom(struct ncplane* n, int* restrict pxy, int* restrict pxx,
                        int* restrict celldimy, int* restrict celldimx,
                        int* restrict maxbmapy, int* restrict maxbmapx);
@@ -1832,10 +1830,10 @@ typedef struct nccell {
 #define CELL_BG_PALETTE         0x0000000008000000ull
 #define CELL_FG_PALETTE         (CELL_BG_PALETTE << 32u)
 #define NCCHANNEL_ALPHA_MASK    0x30000000ull
-#define CELL_ALPHA_HIGHCONTRAST 0x30000000ull
-#define CELL_ALPHA_TRANSPARENT  0x20000000ull
-#define CELL_ALPHA_BLEND        0x10000000ull
-#define CELL_ALPHA_OPAQUE       0x00000000ull
+#define NCALPHA_HIGHCONTRAST 0x30000000ull
+#define NCALPHA_TRANSPARENT  0x20000000ull
+#define NCALPHA_BLEND        0x10000000ull
+#define NCALPHA_OPAQUE       0x00000000ull
 ```
 
 `nccell`s must be initialized with an initialization macro or `nccell_init()`
@@ -2888,7 +2886,7 @@ channel_set_alpha(unsigned* channel, unsigned alpha){
     return -1;
   }
   *channel = alpha | (*channel & ~CHANNEL_ALPHA_MASK);
-  if(alpha != CELL_ALPHA_OPAQUE){
+  if(alpha != NCALPHA_OPAQUE){
     *channel |= CELL_BGDEFAULT_MASK;
   }
   return 0;
@@ -3013,7 +3011,7 @@ ncchannels_set_fg_alpha(uint64_t* channels, unsigned alpha){
 // Set the 2-bit alpha component of the background channel.
 static inline int
 ncchannels_set_bg_alpha(uint64_t* channels, unsigned alpha){
-  if(alpha == CELL_ALPHA_HIGHCONTRAST){ // forbidden for background alpha
+  if(alpha == NCALPHA_HIGHCONTRAST){ // forbidden for background alpha
     return -1;
   }
   unsigned channel = ncchannels_bchannel(*channels);
@@ -3185,7 +3183,7 @@ int notcurses_lex_blitter(const char* op, ncblitter_e* blitter);
 const char* notcurses_str_blitter(ncblitter_e blitter);
 
 #define NCVISUAL_OPTION_NODEGRADE     0x0001ull // fail rather than degrade
-#define NCVISUAL_OPTION_BLEND         0x0002ull // use CELL_ALPHA_BLEND
+#define NCVISUAL_OPTION_BLEND         0x0002ull // use NCALPHA_BLEND
 #define NCVISUAL_OPTION_HORALIGNED    0x0004ull // x is an alignment, not abs
 #define NCVISUAL_OPTION_VERALIGNED    0x0008ull // y is an alignment, not abs
 #define NCVISUAL_OPTION_ADDALPHA      0x0010ull // transcolor is in effect
diff --git a/doc/HACKING.md b/doc/HACKING.md
index 67949cb203..540e431678 100644
--- a/doc/HACKING.md
+++ b/doc/HACKING.md
@@ -193,7 +193,7 @@ there is no resize step between rasterizing and writing, writing deals with
 the same geometries as rasterization, so we ignore it.
 
 Rasterization can be split into two virtual phases: *postpaint* and *rastering*.
-*postpaint* corrects for `CELL_ALPHA_HIGHCONTRAST`, performs damage detection,
+*postpaint* corrects for `NCALPHA_HIGHCONTRAST`, performs damage detection,
 and copies any necessary EGCs from their source pools to the common pool
 (copying these EGCs is why a pile cannot be modified between rendering and
 rasterizing--such modifications might invalidate the EGC references). The
@@ -366,3 +366,50 @@ be a multiple of six pixels tall.
 * always starts at the origin of its plane
 * admits no other output to its plane, nor resizing
 * greatly complicates rendering
+
+## Input
+
+Input is greatly complicated by rare but critical in-band signaling from the
+terminal itself. This is the method by which, for instance, terminals
+advertising Sixel indicate how many color registers they support. We must
+ensure such responses never reach the user, and that we act on them quickly.
+Such replies are generally distinguished by a (literal) escape. Unfortunately,
+the user can (and often does) generate ESC themselves.
+
+The primary instance of this signaling is on startup, when we query the
+terminal as part of capability discovery. Until we process the reply, we
+don't know what capabilities the terminal offers, particularly with regard
+to bitmap graphics.
+
+We have two potential input sources, both of which *might* correspond to
+`stdin`. If we were spawned attached to the terminal, we receive both user and
+terminal input on the same fd (corresponding to `stdin`). If our input was
+redirected from somewhere else, we need open the controlling terminal, and
+read from it. This has the happy side-effect of isolating the control plane
+from the data plane (though you mustn't rely that this will make control
+communication unforgeable; the user can likely write to the controlling
+terminal themselves).
+
+If a terminal doesn't understand or implement some query, there will typically
+be no response. If a negative response is required, follow up the query (or
+queries) with a Device Attributes (DA, `\e[c`) query, to which all known terminals
+will respond. So long as a valid response cannot be confused with a response to
+DA, this serves as a negative acknowledgement. Relying on this, at startup we
+fire off two `XTSMGRAPHICS` queries followed by a DA query, all as one write. We
+don't sit around waiting for the response, but instead continue initialization.
+Ideally, by the time we're done and need the info, it's ready for us to read.
+
+Some inputs intended for the user are transmitted to us as escapes, however.
+Any of the synthesized characters (including e.g. Home, function keys, arrows)
+arrive as escapes, which we convert to codepoints in the Private Use Area.
+These need be delivered to the user.
+
+There are no asynchronous control messages that we need watch for (the closest
+thing is `SIGWINCH` on geometry changes), so we don't generally need to watch
+the input. We *do* need to extract any control messages that arrive while the
+user is reading input (when `stdin` is connected to the tty, anyway).
+Similarly, were we reading, we'd need put aside any input intended for the
+user. We thus keep two queues at all times: received control messages, and
+received user input. The received user input is non-segmented UTF-8 (i.e.
+translated from control sequences). The received control information is stored
+as distinct multibyte escape sequences.
diff --git a/doc/man/man3/notcurses_capabilities.3.md b/doc/man/man3/notcurses_capabilities.3.md
index bbe841ee0b..eba29bc36a 100644
--- a/doc/man/man3/notcurses_capabilities.3.md
+++ b/doc/man/man3/notcurses_capabilities.3.md
@@ -87,8 +87,7 @@ quadrants and halfblocks, respectively. **notcurses_canbraille** returns
 these functions return **true** unless UTF-8 encoding is in use.
 
 **notcurses_check_pixel_support** returns 1 if bitmap support (via any
-mechanism) is detected; **NCBLIT_PIXEL** can be used after such a return.
-It returns 0 a lack of bitmap support was confirmed, and -1 on error.
+mechanism) has been detected, and otherwise 0.
 
 # NOTES
 
diff --git a/doc/man/man3/notcurses_cell.3.md b/doc/man/man3/notcurses_cell.3.md
index 131f4034f1..e6efe47268 100644
--- a/doc/man/man3/notcurses_cell.3.md
+++ b/doc/man/man3/notcurses_cell.3.md
@@ -27,17 +27,17 @@ typedef struct nccell {
 #define CELL_INITIALIZER(c, s, chan) \
  { .gcluster = (c), .stylemask = (s), .channels = (chan), }
 
-#define CELL_BGDEFAULT_MASK     0x0000000040000000ull
-#define CELL_FGDEFAULT_MASK     (CELL_BGDEFAULT_MASK << 32u)
-#define CELL_BG_RGB_MASK        0x0000000000ffffffull
-#define CELL_FG_RGB_MASK        (CELL_BG_MASK << 32u)
-#define CELL_BG_PALETTE         0x0000000008000000ull
-#define CELL_FG_PALETTE         (CELL_BG_PALETTE << 32u)
-#define CHANNEL_ALPHA_MASK      0x30000000ull
-#define CELL_ALPHA_HIGHCONTRAST 0x30000000ull
-#define CELL_ALPHA_TRANSPARENT  0x20000000ull
-#define CELL_ALPHA_BLEND        0x10000000ull
-#define CELL_ALPHA_OPAQUE       0x00000000ull
+#define CELL_BGDEFAULT_MASK   0x0000000040000000ull
+#define CELL_FGDEFAULT_MASK   (CELL_BGDEFAULT_MASK << 32u)
+#define CELL_BG_RGB_MASK      0x0000000000ffffffull
+#define CELL_FG_RGB_MASK      (CELL_BG_MASK << 32u)
+#define CELL_BG_PALETTE       0x0000000008000000ull
+#define CELL_FG_PALETTE       (CELL_BG_PALETTE << 32u)
+#define CHANNEL_ALPHA_MASK    0x30000000ull
+#define NCALPHA_HIGHCONTRAST  0x30000000ull
+#define NCALPHA_TRANSPARENT   0x20000000ull
+#define NCALPHA_BLEND         0x10000000ull
+#define NCALPHA_OPAQUE        0x00000000ull
 ```
 
 **void nccell_init(nccell* ***c***);**
diff --git a/doc/man/man3/notcurses_plane.3.md b/doc/man/man3/notcurses_plane.3.md
index 8dee4f0b23..f383946d9f 100644
--- a/doc/man/man3/notcurses_plane.3.md
+++ b/doc/man/man3/notcurses_plane.3.md
@@ -363,10 +363,7 @@ plane when scrolling is enabled).
 return the size of the plane in pixels. **celldimy** and **celldimx** return
 the size of a cell in pixels (these ought be the same across planes).
 **maxbmapy** and **maxbmapx** describe the largest bitmap which can be
-displayed in the plane. This function transitively calls
-**notcurses_check_pixel_support**, possibly leading to terminal interrogation
-(see **notcurses_capabilities(3)** for why this may be undesirable). Any
-parameter (save **n**) may be **NULL**.
+displayed in the plane. Any parameter (save **n**) may be **NULL**.
 
 When a plane is blitted to using **ncvisual_render** and **NCBLIT_PIXEL** (see
 **notcurses_visual(3)**), it ceases to accept cell-based output. The sprixel
diff --git a/doc/man/man3/notcurses_render.3.md b/doc/man/man3/notcurses_render.3.md
index f7335c9499..2c870fd3eb 100644
--- a/doc/man/man3/notcurses_render.3.md
+++ b/doc/man/man3/notcurses_render.3.md
@@ -85,8 +85,8 @@ At each plane **P**, we consider a cell **C**. This cell is the intersecting cel
 unless that cell has no EGC. In that case, **C** is the plane's default cell.
 
 * If we have not yet determined an EGC, and **C** has a non-zero EGC, use the EGC and style of **C**.
-* If we have not yet locked in a foreground color, and **C** is not foreground-transparent, use the foreground color of **C** (see [BUGS][] below). If **C** is **CELL_ALPHA_OPAQUE**, lock the color in.
-* If we have not yet locked in a background color, and **C** is not background-transparent, use the background color of **C** (see [BUGS][] below). If **C** is **CELL_ALPHA_OPAQUE**, lock the color in.
+* If we have not yet locked in a foreground color, and **C** is not foreground-transparent, use the foreground color of **C** (see [BUGS][] below). If **C** is **NCALPHA_OPAQUE**, lock the color in.
+* If we have not yet locked in a background color, and **C** is not background-transparent, use the background color of **C** (see [BUGS][] below). If **C** is **NCALPHA_OPAQUE**, lock the color in.
 
 If the algorithm concludes without an EGC, the cell is rendered with no glyph
 and a default background. If the algorithm concludes without a color locked in,
diff --git a/doc/man/man3/notcurses_visual.3.md b/doc/man/man3/notcurses_visual.3.md
index c3480a0250..067364a27e 100644
--- a/doc/man/man3/notcurses_visual.3.md
+++ b/doc/man/man3/notcurses_visual.3.md
@@ -150,7 +150,7 @@ subsection of the **ncvisual** to render, while ***leny***/***lenx*** specify th
 geometry of same. ***flags*** is a bitfield over:
 
 * **NCVISUAL_OPTION_NODEGRADE** If the specified blitter is not available, fail rather than degrading.
-* **NCVISUAL_OPTION_BLEND**: Render with **CELL_ALPHA_BLEND**. Not available with
+* **NCVISUAL_OPTION_BLEND**: Render with **NCALPHA_BLEND**. Not available with
    **NCBLIT_PIXEL** when using Sixel graphics. When used with **NCBLIT_PIXEL** when
    using Kitty graphics, the alpha channel is divided by 2 for each pixel.
 * **NCVISUAL_OPTION_HORALIGNED**: Interpret ***x*** as an **ncalign_e**.
@@ -254,14 +254,6 @@ information.
 # PIXEL BLITTING
 
 Some terminals support pixel-based output via one of a number of protocols.
-Checking for bitmap support requires interrogating the terminal and reading a
-response. This takes time, and will never complete if the terminal doesn't
-respond. Notcurses will not do so without an explicit request from the client
-code. Before **NCBLIT_PIXEL** can be used, it is thus necessary to call
-**notcurses_check_pixel_support**. If this function has not successfully
-returned, attempts to use **NCBLIT_PIXEL** will fall back to cell-based
-blitting (or fail, if **NCVISUAL_OPTION_NODEGRADE** is used).
-
 **NCBLIT_PIXEL** has some stringent requirements on the type of planes it can
 be used with; it is usually best to let **ncvisual_render** create the backing
 plane by providing a **NULL** value for **n**. If you must bring your own
diff --git a/include/notcurses/direct.h b/include/notcurses/direct.h
index 717b3a11a3..4442784577 100644
--- a/include/notcurses/direct.h
+++ b/include/notcurses/direct.h
@@ -246,9 +246,7 @@ API bool ncdirect_canopen_images(const struct ncdirect* n)
 API bool ncdirect_canutf8(const struct ncdirect* n)
   __attribute__ ((nonnull (1)));
 
-// This function must successfully return before NCBLIT_PIXEL is available.
-// Returns -1 on error, 0 for no support, or 1 if pixel output is supported.
-// Must not be called concurrently with either input or rasterization.
+// Can we blit pixel-accurate bitmaps?
 API int ncdirect_check_pixel_support(struct ncdirect* n)
   __attribute__ ((nonnull (1)));
 
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index 70ef5571ac..7d34f55648 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -113,10 +113,10 @@ API int notcurses_ucs32_to_utf8(const char32_t* ucs32, unsigned ucs32count,
                                 unsigned char* resultbuf, size_t buflen);
 
 // background cannot be highcontrast, only foreground
-#define CELL_ALPHA_HIGHCONTRAST 0x30000000ull
-#define CELL_ALPHA_TRANSPARENT  0x20000000ull
-#define CELL_ALPHA_BLEND        0x10000000ull
-#define CELL_ALPHA_OPAQUE       0x00000000ull
+#define NCALPHA_HIGHCONTRAST    0x30000000ull
+#define NCALPHA_TRANSPARENT     0x20000000ull
+#define NCALPHA_BLEND           0x10000000ull
+#define NCALPHA_OPAQUE          0x00000000ull
 
 // if this bit is set, we are *not* using the default background color
 #define CELL_BGDEFAULT_MASK     0x0000000040000000ull
@@ -248,7 +248,7 @@ ncchannel_set_alpha(unsigned* channel, unsigned alpha){
     return -1;
   }
   *channel = alpha | (*channel & ~CELL_BG_ALPHA_MASK);
-  if(alpha != CELL_ALPHA_OPAQUE){
+  if(alpha != NCALPHA_OPAQUE){
     *channel |= CELL_BGDEFAULT_MASK;
   }
   return 0;
@@ -261,7 +261,7 @@ ncchannel_set_palindex(uint32_t* channel, int idx){
   }
   *channel |= CELL_BGDEFAULT_MASK;
   *channel |= CELL_BG_PALETTE;
-  ncchannel_set_alpha(channel, CELL_ALPHA_OPAQUE);
+  ncchannel_set_alpha(channel, NCALPHA_OPAQUE);
   *channel &= 0xff000000ull;
   *channel |= idx;
   return 0;
@@ -282,7 +282,7 @@ ncchannel_palindex_p(unsigned channel){
 // Mark the channel as using its default color, which also marks it opaque.
 static inline unsigned
 ncchannel_set_default(unsigned* channel){
-  return *channel &= ~(CELL_BGDEFAULT_MASK | CELL_ALPHA_HIGHCONTRAST);
+  return *channel &= ~(CELL_BGDEFAULT_MASK | NCALPHA_HIGHCONTRAST);
 }
 
 // Extract the 32-bit background channel from a channel pair.
@@ -438,7 +438,7 @@ ncchannels_set_bg_rgb8_clipped(uint64_t* channels, int r, int g, int b){
 // Set the 2-bit alpha component of the background channel.
 static inline int
 ncchannels_set_bg_alpha(uint64_t* channels, unsigned alpha){
-  if(alpha == CELL_ALPHA_HIGHCONTRAST){ // forbidden for background alpha
+  if(alpha == NCALPHA_HIGHCONTRAST){ // forbidden for background alpha
     return -1;
   }
   uint32_t channel = ncchannels_bchannel(*channels);
@@ -1122,10 +1122,8 @@ ncplane_dim_x(const struct ncplane* n){
 
 // Retrieve pixel geometry for the display region ('pxy', 'pxx'), each cell
 // ('celldimy', 'celldimx'), and the maximum displayable bitmap ('maxbmapy',
-// 'maxbmapx'). Note that this will call notcurses_check_pixel_support(),
-// possibly leading to an interrogation of the terminal. If bitmaps are not
-// supported, 'maxbmapy' and 'maxbmapx' will be 0. Any of the geometry
-// arguments may be NULL.
+// 'maxbmapx'). If bitmaps are not supported, 'maxbmapy' and 'maxbmapx' will
+// be 0. Any of the geometry arguments may be NULL.
 API void ncplane_pixelgeom(struct ncplane* n, int* RESTRICT pxy, int* RESTRICT pxx,
                            int* RESTRICT celldimy, int* RESTRICT celldimx,
                            int* RESTRICT maxbmapy, int* RESTRICT maxbmapx)
@@ -1292,9 +1290,7 @@ API bool notcurses_cansextant(const struct notcurses* nc)
 API bool notcurses_canbraille(const struct notcurses* nc)
   __attribute__ ((nonnull (1)));
 
-// This function must successfully return before NCBLIT_PIXEL is available.
-// Returns -1 on error, 0 for no support, or 1 if pixel output is supported.
-// Must not be called concurrently with either input or rasterization.
+// Can we blit pixel-accurate bitmaps?
 API int notcurses_check_pixel_support(struct notcurses* nc)
   __attribute__ ((nonnull (1)));
 
@@ -2480,7 +2476,7 @@ API ALLOC struct ncvisual* ncvisual_from_plane(const struct ncplane* n,
                                                int leny, int lenx);
 
 #define NCVISUAL_OPTION_NODEGRADE     0x0001ull // fail rather than degrade
-#define NCVISUAL_OPTION_BLEND         0x0002ull // use CELL_ALPHA_BLEND with visual
+#define NCVISUAL_OPTION_BLEND         0x0002ull // use NCALPHA_BLEND with visual
 #define NCVISUAL_OPTION_HORALIGNED    0x0004ull // x is an alignment, not absolute
 #define NCVISUAL_OPTION_VERALIGNED    0x0008ull // y is an alignment, not absolute
 #define NCVISUAL_OPTION_ADDALPHA      0x0010ull // transcolor is in effect
@@ -4316,6 +4312,11 @@ typedef ncpalette palette256;
 
 typedef nccell cell; // FIXME backwards-compat, remove in ABI3
 
+#define CELL_ALPHA_HIGHCONTRAST NCALPHA_HIGHCONTRAST
+#define CELL_ALPHA_TRANSPARENT  NCALPHA_TRANSPARENT
+#define CELL_ALPHA_BLEND        NCALPHA_BLEND
+#define CELL_ALPHA_OPAQUE       NCALPHA_OPAQUE
+
 #undef ALLOC
 #undef API
 
diff --git a/rust/src/bindings.rs b/rust/src/bindings.rs
index 43b809fea1..0f7895b7e6 100644
--- a/rust/src/bindings.rs
+++ b/rust/src/bindings.rs
@@ -40,10 +40,10 @@ pub use ffi::{
 // cell,
 //
 // // constants
-// CELL_ALPHA_BLEND,
-// CELL_ALPHA_HIGHCONTRAST,
-// CELL_ALPHA_OPAQUE,
-// CELL_ALPHA_TRANSPARENT,
+// NCALPHA_BLEND,
+// NCALPHA_HIGHCONTRAST,
+// NCALPHA_OPAQUE,
+// NCALPHA_TRANSPARENT,
 // CELL_BGDEFAULT_MASK,
 // CELL_BG_ALPHA_MASK,
 // CELL_BG_PALETTE,
diff --git a/rust/src/cells/methods.rs b/rust/src/cells/methods.rs
index 4652e84bfd..9d2a0f4191 100644
--- a/rust/src/cells/methods.rs
+++ b/rust/src/cells/methods.rs
@@ -227,7 +227,7 @@ impl NcCell {
     /// Sets the background [NcPaletteIndex].
     ///
     /// Also sets [NCCELL_BG_PALETTE][crate::NCCELL_BG_PALETTE] and
-    /// [NCCELL_ALPHA_OPAQUE][crate::NCCELL_ALPHA_OPAQUE], and clears out
+    /// [NCCELL_OPAQUE][crate::NCCELL_OPAQUE], and clears out
     /// [NCCELL_BGDEFAULT_MASK][crate::NCCELL_BGDEFAULT_MASK].
     ///
     /// *C style function: [nccell_set_bg_palindex()][crate::nccell_set_bg_palindex].*
@@ -267,7 +267,7 @@ impl NcCell {
     /// Sets the foreground [NcPaletteIndex].
     ///
     /// Also sets [NCCELL_FG_PALETTE][crate::NCCELL_FG_PALETTE] and
-    /// [NCCELL_ALPHA_OPAQUE][crate::NCCELL_ALPHA_OPAQUE], and clears out
+    /// [NCCELL_OPAQUE][crate::NCCELL_OPAQUE], and clears out
     /// [NCCELL_BGDEFAULT_MASK][crate::NCCELL_BGDEFAULT_MASK].
     ///
     /// *C style function: [nccell_set_fg_palindex()][crate::nccell_set_fg_palindex].*
diff --git a/rust/src/cells/mod.rs b/rust/src/cells/mod.rs
index bc2cb703ea..960aa7a892 100644
--- a/rust/src/cells/mod.rs
+++ b/rust/src/cells/mod.rs
@@ -161,19 +161,19 @@ pub use reimplemented::*;
 /// have two bits of inverted alpha. The actual grapheme written to a cell is
 /// the topmost non-zero grapheme.
 ///
-/// - If its alpha is 00 ([`NCCELL_ALPHA_OPAQUE`]) its foreground color is used unchanged.
+/// - If its alpha is 00 ([`NCCELL_OPAQUE`]) its foreground color is used unchanged.
 ///
-/// - If its alpha is 10 ([`NCCELL_ALPHA_TRANSPARENT`]) its foreground color is derived
+/// - If its alpha is 10 ([`NCCELL_TRANSPARENT`]) its foreground color is derived
 ///   entirely from cells underneath it.
 ///
-/// - If its alpha is 01 ([`NCCELL_ALPHA_BLEND`]) the result will be a composite.
+/// - If its alpha is 01 ([`NCCELL_BLEND`]) the result will be a composite.
 ///
 /// Likewise for the background. If the bottom of a coordinate's zbuffer is
 /// reached with a cumulative alpha of zero, the default is used. In this way,
 /// a terminal configured with transparent background can be supported through
 /// multiple occluding ncplanes.
 ///
-/// A foreground alpha of 11 ([`NCCELL_ALPHA_HIGHCONTRAST`]) requests high-contrast
+/// A foreground alpha of 11 ([`NCCELL_HIGHCONTRAST`]) requests high-contrast
 /// text (relative to the computed background).
 /// A background alpha of 11 is currently forbidden.
 ///
@@ -206,21 +206,21 @@ use crate::{NcAlphaBits, NcChannel, NcPlane};
 /// [`NcAlphaBits`] bits indicating
 /// [`NcCell`]'s foreground or background color will be a composite between
 /// its color and the `NcCell`s' corresponding colors underneath it
-pub const NCCELL_ALPHA_BLEND: u32 = crate::bindings::ffi::CELL_ALPHA_BLEND;
+pub const NCCELL_BLEND: u32 = crate::bindings::ffi::NCALPHA_BLEND;
 
 /// [`NcAlphaBits`] bits indicating
 /// [`NcCell`]'s foreground color will be high-contrast (relative to the
 /// computed background). Background cannot be highcontrast
-pub const NCCELL_ALPHA_HIGHCONTRAST: u32 = crate::bindings::ffi::CELL_ALPHA_HIGHCONTRAST;
+pub const NCCELL_HIGHCONTRAST: u32 = crate::bindings::ffi::NCALPHA_HIGHCONTRAST;
 
 /// [`NcAlphaBits`] bits indicating
 /// [`NcCell`]'s foreground or background color is used unchanged
-pub const NCCELL_ALPHA_OPAQUE: u32 = crate::bindings::ffi::CELL_ALPHA_OPAQUE;
+pub const NCCELL_OPAQUE: u32 = crate::bindings::ffi::NCALPHA_OPAQUE;
 
 /// [`NcAlphaBits`] bits indicating
 /// [`NcCell`]'s foreground or background color is derived entirely from the
 /// `NcCell`s underneath it
-pub const NCCELL_ALPHA_TRANSPARENT: u32 = crate::bindings::ffi::CELL_ALPHA_TRANSPARENT;
+pub const NCCELL_TRANSPARENT: u32 = crate::bindings::ffi::NCALPHA_TRANSPARENT;
 
 /// If this bit is set, we are *not* using the default background color
 ///
diff --git a/rust/src/cells/reimplemented.rs b/rust/src/cells/reimplemented.rs
index cee5b34e87..792e1634c2 100644
--- a/rust/src/cells/reimplemented.rs
+++ b/rust/src/cells/reimplemented.rs
@@ -4,7 +4,7 @@ use libc::strcmp;
 
 use crate::{
     cstring, nccell_release, NcAlphaBits, NcCell, NcChannel, NcChannelPair, NcColor, NcEgc,
-    NcIntResult, NcPaletteIndex, NcPlane, NcRgb, NcStyleMask, NCCELL_ALPHA_OPAQUE,
+    NcIntResult, NcPaletteIndex, NcPlane, NcRgb, NcStyleMask, NCCELL_OPAQUE,
     NCCELL_BGDEFAULT_MASK, NCCELL_BG_PALETTE, NCCELL_FGDEFAULT_MASK, NCCELL_FG_PALETTE,
     NCRESULT_ERR, NCRESULT_OK, NCSTYLE_MASK,
 };
@@ -208,7 +208,7 @@ pub const fn nccell_bg_palindex(cell: &NcCell) -> NcPaletteIndex {
 
 /// Sets an [NcCell]'s foreground [NcPaletteIndex].
 ///
-/// Also sets [NCCELL_FG_PALETTE] and [NCCELL_ALPHA_OPAQUE],
+/// Also sets [NCCELL_FG_PALETTE] and [NCCELL_OPAQUE],
 /// and clears out [NCCELL_FGDEFAULT_MASK].
 ///
 /// *Method: NcCell.[set_fg_palindex()][NcCell#method.set_fg_palindex].*
@@ -219,14 +219,14 @@ pub const fn nccell_bg_palindex(cell: &NcCell) -> NcPaletteIndex {
 pub fn nccell_set_fg_palindex(cell: &mut NcCell, index: NcPaletteIndex) {
     cell.channels |= NCCELL_FGDEFAULT_MASK;
     cell.channels |= NCCELL_FG_PALETTE;
-    nccell_set_fg_alpha(cell, NCCELL_ALPHA_OPAQUE);
+    nccell_set_fg_alpha(cell, NCCELL_OPAQUE);
     cell.channels &= 0xff000000ffffffff as NcChannelPair;
     cell.channels |= (index as NcChannelPair) << 32;
 }
 
 /// Sets an [NcCell]'s background [NcPaletteIndex].
 ///
-/// Also sets [NCCELL_BG_PALETTE] and [NCCELL_ALPHA_OPAQUE],
+/// Also sets [NCCELL_BG_PALETTE] and [NCCELL_OPAQUE],
 /// and clears out [NCCELL_BGDEFAULT_MASK].
 ///
 /// *Method: NcCell.[set_bg_palindex()][NcCell#method.set_bg_palindex].*
@@ -236,7 +236,7 @@ pub fn nccell_set_fg_palindex(cell: &mut NcCell, index: NcPaletteIndex) {
 pub fn nccell_set_bg_palindex(cell: &mut NcCell, index: NcPaletteIndex) {
     cell.channels |= NCCELL_BGDEFAULT_MASK as NcChannelPair;
     cell.channels |= NCCELL_BG_PALETTE as NcChannelPair;
-    nccell_set_bg_alpha(cell, NCCELL_ALPHA_OPAQUE);
+    nccell_set_bg_alpha(cell, NCCELL_OPAQUE);
     cell.channels &= 0xffffffffff000000;
     cell.channels |= index as NcChannelPair;
 }
diff --git a/rust/src/channel/reimplemented.rs b/rust/src/channel/reimplemented.rs
index 535453a179..15c961a106 100644
--- a/rust/src/channel/reimplemented.rs
+++ b/rust/src/channel/reimplemented.rs
@@ -2,7 +2,7 @@
 
 use crate::{
     NcAlphaBits, NcChannel, NcChannelPair, NcColor, NcPaletteIndex, NcRgb,
-    NCCELL_ALPHA_HIGHCONTRAST, NCCELL_ALPHA_OPAQUE, NCCELL_BGDEFAULT_MASK, NCCELL_BG_PALETTE,
+    NCCELL_HIGHCONTRAST, NCCELL_OPAQUE, NCCELL_BGDEFAULT_MASK, NCCELL_BG_PALETTE,
     NCCELL_BG_RGB_MASK, NCCELL_FGDEFAULT_MASK, NCCELL_FG_PALETTE, NCCHANNEL_ALPHA_MASK,
 };
 
@@ -24,7 +24,7 @@ pub fn ncchannel_set_alpha(channel: &mut NcChannel, alpha: NcAlphaBits) {
     let alpha_clean = alpha & NCCHANNEL_ALPHA_MASK;
     *channel = alpha_clean | (*channel & !NCCHANNEL_ALPHA_MASK);
 
-    if alpha != NCCELL_ALPHA_OPAQUE {
+    if alpha != NCCELL_OPAQUE {
         // indicate that we are *not* using the default background color
         *channel |= NCCELL_BGDEFAULT_MASK;
     }
@@ -62,9 +62,9 @@ pub fn ncchannels_set_fg_alpha(channels: &mut NcChannelPair, alpha: NcAlphaBits)
 #[inline]
 pub fn ncchannels_set_bg_alpha(channels: &mut NcChannelPair, alpha: NcAlphaBits) {
     let mut alpha_clean = alpha;
-    if alpha == NCCELL_ALPHA_HIGHCONTRAST {
+    if alpha == NCCELL_HIGHCONTRAST {
         // forbidden for background alpha, so makes it opaque
-        alpha_clean = NCCELL_ALPHA_OPAQUE;
+        alpha_clean = NCCELL_OPAQUE;
     }
     let mut channel = ncchannels_bchannel(*channels);
     ncchannel_set_alpha(&mut channel, alpha_clean);
@@ -345,7 +345,7 @@ pub const fn ncchannel_default_p(channel: NcChannel) -> bool {
 /// *Method: NcChannel.[set_default()][NcChannel#method.set_default]*
 #[inline]
 pub fn ncchannel_set_default(channel: &mut NcChannel) -> NcChannel {
-    *channel &= !(NCCELL_BGDEFAULT_MASK | NCCELL_ALPHA_HIGHCONTRAST);
+    *channel &= !(NCCELL_BGDEFAULT_MASK | NCCELL_HIGHCONTRAST);
     *channel
 }
 
@@ -491,7 +491,7 @@ pub fn ncchannels_bg_palindex_p(channels: NcChannelPair) -> bool {
 pub fn ncchannels_set_fg_palindex(channels: &mut NcChannelPair, index: NcPaletteIndex) {
     *channels |= NCCELL_FGDEFAULT_MASK;
     *channels |= NCCELL_FG_PALETTE as NcChannelPair;
-    ncchannels_set_fg_alpha(channels, NCCELL_ALPHA_OPAQUE);
+    ncchannels_set_fg_alpha(channels, NCCELL_OPAQUE);
     *channels &= 0xff000000ffffffff as NcChannelPair;
     *channels |= (index as NcChannelPair) << 32;
 }
@@ -504,7 +504,7 @@ pub fn ncchannels_set_fg_palindex(channels: &mut NcChannelPair, index: NcPalette
 pub fn ncchannels_set_bg_palindex(channels: &mut NcChannelPair, index: NcPaletteIndex) {
     *channels |= NCCELL_BGDEFAULT_MASK as NcChannelPair;
     *channels |= NCCELL_BG_PALETTE as NcChannelPair;
-    ncchannels_set_bg_alpha(channels, NCCELL_ALPHA_OPAQUE);
+    ncchannels_set_bg_alpha(channels, NCCELL_OPAQUE);
     *channels &= 0xffffffffff000000;
     *channels |= index as NcChannelPair;
 }
diff --git a/rust/src/visual/mod.rs b/rust/src/visual/mod.rs
index 8e63f3f436..e5f4c9ac7f 100644
--- a/rust/src/visual/mod.rs
+++ b/rust/src/visual/mod.rs
@@ -134,7 +134,7 @@ pub type NcRgba = u32;
 /// Treats as transparent the color specified in the `transcolor` field.
 pub const NCVISUAL_OPTION_ADDALPHA: u32 = crate::bindings::ffi::NCVISUAL_OPTION_ADDALPHA;
 
-/// Uses [`NCCELL_ALPHA_BLEND`][crate::NCCELL_ALPHA_BLEND] with visual.
+/// Uses [`NCCELL_BLEND`][crate::NCCELL_BLEND] with visual.
 pub const NCVISUAL_OPTION_BLEND: u32 = crate::bindings::ffi::NCVISUAL_OPTION_BLEND;
 
 /// allows you to indicate that the n field of ncvisual_options refers not to
diff --git a/src/demo/boxdemo.c b/src/demo/boxdemo.c
index 965190f12e..f044635971 100644
--- a/src/demo/boxdemo.c
+++ b/src/demo/boxdemo.c
@@ -165,8 +165,8 @@ int box_demo(struct notcurses* nc){
   struct ncplane* n = notcurses_stddim_yx(nc, &ylen, &xlen);
   ncplane_erase(n);
   uint64_t transchan = 0;
-  ncchannels_set_bg_alpha(&transchan, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_fg_alpha(&transchan, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&transchan, NCALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&transchan, NCALPHA_TRANSPARENT);
   ncplane_set_base(n, "", 0, transchan);
   nccell ul = CELL_TRIVIAL_INITIALIZER, ll = CELL_TRIVIAL_INITIALIZER;
   nccell lr = CELL_TRIVIAL_INITIALIZER, ur = CELL_TRIVIAL_INITIALIZER;
diff --git a/src/demo/chunli.c b/src/demo/chunli.c
index ec8f10a241..45125df30b 100644
--- a/src/demo/chunli.c
+++ b/src/demo/chunli.c
@@ -52,8 +52,8 @@ int chunli_demo(struct notcurses* nc){
   int ret, dimx, dimy;
   notcurses_refresh(nc, &dimy, &dimx);
   nccell b = CELL_TRIVIAL_INITIALIZER;
-  nccell_set_fg_alpha(&b, CELL_ALPHA_TRANSPARENT);
-  nccell_set_bg_alpha(&b, CELL_ALPHA_TRANSPARENT);
+  nccell_set_fg_alpha(&b, NCALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(&b, NCALPHA_TRANSPARENT);
   if( (ret = chunli_draw(nc, "bmp", CHUNS, &b)) ){
     return ret;
   }
diff --git a/src/demo/eagle.c b/src/demo/eagle.c
index a83f5b2d30..2dff3e355b 100644
--- a/src/demo/eagle.c
+++ b/src/demo/eagle.c
@@ -116,8 +116,8 @@ zoom_map(struct notcurses* nc, const char* map, int* ret){
 static int
 draw_eagle(struct ncplane* n, const char* sprite){
   nccell bgc = CELL_TRIVIAL_INITIALIZER;
-  nccell_set_fg_alpha(&bgc, CELL_ALPHA_TRANSPARENT);
-  nccell_set_bg_alpha(&bgc, CELL_ALPHA_TRANSPARENT);
+  nccell_set_fg_alpha(&bgc, NCALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(&bgc, NCALPHA_TRANSPARENT);
   ncplane_set_base_cell(n, &bgc);
   nccell_release(n, &bgc);
   size_t s;
diff --git a/src/demo/highcon.c b/src/demo/highcon.c
index 51ceace208..02da2038f5 100644
--- a/src/demo/highcon.c
+++ b/src/demo/highcon.c
@@ -75,7 +75,7 @@ int highcontrast_demo(struct notcurses* nc){
   }
   const char motto[] = " high contrast text is evaluated relative to the solved background";
   nccell c = CELL_TRIVIAL_INITIALIZER;
-  nccell_set_fg_alpha(&c, CELL_ALPHA_HIGHCONTRAST);
+  nccell_set_fg_alpha(&c, NCALPHA_HIGHCONTRAST);
   unsigned total = 0, r = 0, g = 0, b = 0;
   for(int out = 0 ; out < totcells ; ++out){ // build up the initial screen
     scrcolors[out] = generate_next_color(&total, &r, &g, &b, STEP);
@@ -99,20 +99,20 @@ int highcontrast_demo(struct notcurses* nc){
   uint64_t iterns = totalns / (totcells / 2);
   do{
     if(offset){
-      nccell_set_fg_alpha(&c, CELL_ALPHA_OPAQUE);
+      nccell_set_fg_alpha(&c, NCALPHA_OPAQUE);
       const int f = offset - 1 + dimx;
       const int l = totcells + dimx - offset;
       ncplane_at_yx_cell(n, f / dimx, f % dimx, &c);
       nccell_set_fg_rgb(&c, 0x004000 + (16 * offset));
       nccell_set_bg_rgb(&c, 0);
-      nccell_set_fg_alpha(&c, CELL_ALPHA_OPAQUE);
+      nccell_set_fg_alpha(&c, NCALPHA_OPAQUE);
       if(ncplane_putc_yx(n, f / dimx, f % dimx, &c) < 0){
         goto err;
       }
       ncplane_at_yx_cell(n, l / dimx, l % dimx, &c);
       nccell_set_fg_rgb(&c, 0x004000 + (16 * offset));
       nccell_set_bg_rgb(&c, 0);
-      nccell_set_fg_alpha(&c, CELL_ALPHA_OPAQUE);
+      nccell_set_fg_alpha(&c, NCALPHA_OPAQUE);
       if(ncplane_putc_yx(n, l / dimx, l % dimx, &c) < 0){
         goto err;
       }
diff --git a/src/demo/hud.c b/src/demo/hud.c
index a1352f620b..b0b4e44e2a 100644
--- a/src/demo/hud.c
+++ b/src/demo/hud.c
@@ -56,9 +56,9 @@ static struct ncplane* debug; // "debug info" modal popup
 static int
 hud_standard_bg_rgb(struct ncplane* n){
   uint64_t channels = 0;
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_BLEND);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_BLEND);
   ncchannels_set_fg_rgb8(&channels, 0x80, 0x80, 0x80);
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_BLEND);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_BLEND);
   ncchannels_set_bg_rgb8(&channels, 0x80, 0x80, 0x80);
   if(ncplane_set_base(n, "", 0, channels) >= 0){
     return -1;
@@ -114,7 +114,7 @@ debug_toggle(struct notcurses* nc){
     return;
   }
   uint64_t channels = 0;
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
   ncchannels_set_bg_rgb(&channels, 0xffffe5);
   ncplane_set_base(n, " ", 0, channels);
   ncplane_set_scrolling(n, true);
@@ -127,8 +127,8 @@ debug_toggle(struct notcurses* nc){
   }
   for(int y = 0 ; y < ncplane_dim_y(n) ; ++y){
     nccell c = CELL_TRIVIAL_INITIALIZER;
-    nccell_set_fg_alpha(&c, CELL_ALPHA_TRANSPARENT);
-    nccell_set_bg_alpha(&c, CELL_ALPHA_TRANSPARENT);
+    nccell_set_fg_alpha(&c, NCALPHA_TRANSPARENT);
+    nccell_set_bg_alpha(&c, NCALPHA_TRANSPARENT);
     ncplane_putc_yx(n, y, ncplane_dim_x(n) - 1, &c);
     nccell_release(n, &c);
   }
@@ -159,14 +159,14 @@ about_toggle(struct notcurses* nc){
   struct ncplane* n = ncplane_create(notcurses_stdplane(nc), &nopts);
   // let the glyphs below show through, but only dimly
   uint64_t channels = 0;
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_BLEND);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_BLEND);
   ncchannels_set_fg_rgb8(&channels, 0x0, 0x0, 0x0);
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_BLEND);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_BLEND);
   ncchannels_set_bg_rgb8(&channels, 0x0, 0x0, 0x0);
   if(ncplane_set_base(n, "", 0, channels) >= 0){
     ncplane_set_fg_rgb(n, 0x11ffff);
     ncplane_set_bg_rgb(n, 0);
-    ncplane_set_bg_alpha(n, CELL_ALPHA_BLEND);
+    ncplane_set_bg_alpha(n, NCALPHA_BLEND);
     ncplane_printf_aligned(n, 1, NCALIGN_CENTER, "notcurses-demo %s", notcurses_version());
     ncplane_printf_aligned(n, 3, NCALIGN_LEFT, "  P toggle plot");
     ncplane_printf_aligned(n, 3, NCALIGN_RIGHT, "toggle help Ctrl+U  ");
@@ -318,11 +318,11 @@ struct ncmenu* menu_create(struct notcurses* nc){
   uint64_t sectionchannels = 0;
   ncchannels_set_fg_rgb(&sectionchannels, 0xffffff);
   ncchannels_set_bg_rgb(&sectionchannels, 0x000000);
-  ncchannels_set_fg_alpha(&sectionchannels, CELL_ALPHA_HIGHCONTRAST);
-  ncchannels_set_bg_alpha(&sectionchannels, CELL_ALPHA_BLEND);
+  ncchannels_set_fg_alpha(&sectionchannels, NCALPHA_HIGHCONTRAST);
+  ncchannels_set_bg_alpha(&sectionchannels, NCALPHA_BLEND);
   ncchannels_set_fg_rgb(&headerchannels, 0xffffff);
   ncchannels_set_bg_rgb(&headerchannels, 0x7f347f);
-  ncchannels_set_bg_alpha(&headerchannels, CELL_ALPHA_BLEND);
+  ncchannels_set_bg_alpha(&headerchannels, NCALPHA_BLEND);
   const ncmenu_options mopts = {
     .sections = sections,
     .sectioncount = sizeof(sections) / sizeof(*sections),
@@ -349,12 +349,12 @@ hud_refresh(struct ncplane* n){
   lr.channels = CHANNELS_RGB_INITIALIZER(0xf0, 0xc0, 0xc0, 0, 0, 0);
   hl.channels = CHANNELS_RGB_INITIALIZER(0xf0, 0xc0, 0xc0, 0, 0, 0);
   vl.channels = CHANNELS_RGB_INITIALIZER(0xf0, 0xc0, 0xc0, 0, 0, 0);
-  nccell_set_bg_alpha(&ul, CELL_ALPHA_BLEND);
-  nccell_set_bg_alpha(&ur, CELL_ALPHA_BLEND);
-  nccell_set_bg_alpha(&ll, CELL_ALPHA_BLEND);
-  nccell_set_bg_alpha(&lr, CELL_ALPHA_BLEND);
-  nccell_set_bg_alpha(&hl, CELL_ALPHA_BLEND);
-  nccell_set_bg_alpha(&vl, CELL_ALPHA_BLEND);
+  nccell_set_bg_alpha(&ul, NCALPHA_BLEND);
+  nccell_set_bg_alpha(&ur, NCALPHA_BLEND);
+  nccell_set_bg_alpha(&ll, NCALPHA_BLEND);
+  nccell_set_bg_alpha(&lr, NCALPHA_BLEND);
+  nccell_set_bg_alpha(&hl, NCALPHA_BLEND);
+  nccell_set_bg_alpha(&vl, NCALPHA_BLEND);
   if(ncplane_perimeter(n, &ul, &ur, &ll, &lr, &hl, &vl, 0)){
     nccell_release(n, &ul); nccell_release(n, &ur); nccell_release(n, &hl);
     nccell_release(n, &ll); nccell_release(n, &lr); nccell_release(n, &vl);
@@ -385,9 +385,9 @@ hud_print_finished(elem* list){
       nccell c = CELL_TRIVIAL_INITIALIZER;
       ncplane_base(hud, &c);
       ncplane_set_bg_rgb(hud, nccell_bg_rgb(&c));
-      ncplane_set_bg_alpha(hud, CELL_ALPHA_BLEND);
+      ncplane_set_bg_alpha(hud, NCALPHA_BLEND);
       ncplane_set_fg_rgb(hud, 0xffffff);
-      ncplane_set_fg_alpha(hud, CELL_ALPHA_OPAQUE);
+      ncplane_set_fg_alpha(hud, NCALPHA_OPAQUE);
       nccell_release(hud, &c);
       if(ncplane_printf_yx(hud, line, 1, "%d", e->frames) < 0){
         return -1;
@@ -430,7 +430,7 @@ struct ncplane* hud_create(struct notcurses* nc){
   hud_refresh(n);
   ncplane_set_fg_rgb(n, 0xffffff);
   ncplane_set_bg_rgb(n, 0);
-  ncplane_set_bg_alpha(n, CELL_ALPHA_BLEND);
+  ncplane_set_bg_alpha(n, NCALPHA_BLEND);
   if(hud_hidden){
     ncplane_reparent(n, n);
   }
@@ -598,9 +598,9 @@ int demo_render(struct notcurses* nc){
     nccell c = CELL_TRIVIAL_INITIALIZER;
     ncplane_base(hud, &c);
     ncplane_set_bg_rgb(hud, nccell_bg_rgb(&c));
-    ncplane_set_bg_alpha(hud, CELL_ALPHA_BLEND);
+    ncplane_set_bg_alpha(hud, NCALPHA_BLEND);
     ncplane_set_fg_rgb(hud, 0x80d0ff);
-    ncplane_set_fg_alpha(hud, CELL_ALPHA_OPAQUE);
+    ncplane_set_fg_alpha(hud, NCALPHA_OPAQUE);
     nccell_release(hud, &c);
     ncplane_on_styles(hud, NCSTYLE_BOLD);
     if(ncplane_printf_yx(hud, 1, 1, "%d", elems->frames) < 0){
@@ -648,9 +648,9 @@ int fpsgraph_init(struct notcurses* nc){
   }
   uint32_t style = 0;
   uint64_t channels = 0;
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_BLEND);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_BLEND);
   ncchannels_set_fg_rgb(&channels, 0x201020);
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_BLEND);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_BLEND);
   ncchannels_set_bg_rgb(&channels, 0x201020);
   ncplane_set_base(newp, "", style, channels);
   ncplot_options opts;
@@ -663,10 +663,10 @@ int fpsgraph_init(struct notcurses* nc){
   opts.title = "frames per semisecond";
   ncchannels_set_fg_rgb8(&opts.minchannels, 0x80, 0x80, 0xff);
   ncchannels_set_bg_rgb(&opts.minchannels, 0x201020);
-  ncchannels_set_bg_alpha(&opts.minchannels, CELL_ALPHA_BLEND);
+  ncchannels_set_bg_alpha(&opts.minchannels, NCALPHA_BLEND);
   ncchannels_set_fg_rgb8(&opts.maxchannels, 0x80, 0xff, 0x80);
   ncchannels_set_bg_rgb(&opts.maxchannels, 0x201020);
-  ncchannels_set_bg_alpha(&opts.maxchannels, CELL_ALPHA_BLEND);
+  ncchannels_set_bg_alpha(&opts.maxchannels, NCALPHA_BLEND);
   // takes ownership of newp on all paths
   struct ncuplot* fpsplot = ncuplot_create(newp, &opts, 0, 0);
   if(!fpsplot){
diff --git a/src/demo/keller.c b/src/demo/keller.c
index b8dc8f177a..9b7e4364aa 100644
--- a/src/demo/keller.c
+++ b/src/demo/keller.c
@@ -18,8 +18,8 @@ visualize(struct notcurses* nc, struct ncvisual* ncv){
   ncplane_set_bg_rgb(stdn, 0);
   ncplane_set_styles(stdn, NCSTYLE_BOLD);
   uint64_t channels = 0;
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
   ncplane_set_base(stdn, "", 0, channels);
   for(size_t i = 0 ; i < sizeof(bs) / sizeof(*bs) ; ++i){
     struct ncvisual_options vopts = {
diff --git a/src/demo/luigi.c b/src/demo/luigi.c
index cea3213e58..457e41cea7 100644
--- a/src/demo/luigi.c
+++ b/src/demo/luigi.c
@@ -112,8 +112,8 @@ static const char* luigis[] = {
 static int
 draw_luigi(struct ncplane* n, const char* sprite){
   uint64_t channels = 0;
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
   ncplane_set_base(n, "", 0, channels);
   size_t s;
   int sbytes;
@@ -197,8 +197,8 @@ int luigi_demo(struct notcurses* nc){
     return -1;
   }
   uint64_t channels = 0;
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
   struct ncplane* wmplane = ncvisual_render(nc, wmncv, NULL);
   if(wmplane == NULL){
     ncvisual_destroy(wmncv);
diff --git a/src/demo/outro.c b/src/demo/outro.c
index e79a506663..5b67918da6 100644
--- a/src/demo/outro.c
+++ b/src/demo/outro.c
@@ -95,8 +95,8 @@ videothread(void* vnc){
     return PTHREAD_CANCELED;
   }
   uint64_t trans_channel = 0;
-  ncchannels_set_bg_alpha(&trans_channel, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_fg_alpha(&trans_channel, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&trans_channel, NCALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&trans_channel, NCALPHA_TRANSPARENT);
   ncplane_set_base(apiap, "", 0, trans_channel);
   ncplane_set_fg_rgb8(apiap, 0xc0, 0x40, 0x80);
   ncplane_set_bg_rgb8(apiap, 0, 0, 0);
@@ -138,7 +138,7 @@ outro_message(struct notcurses* nc, int* rows, int* cols){
   ncplane_dim_yx(non, rows, cols);
   int ybase = 0;
   // bevel the upper corners
-  if(ncplane_set_bg_alpha(non, CELL_ALPHA_TRANSPARENT)){
+  if(ncplane_set_bg_alpha(non, NCALPHA_TRANSPARENT)){
     return NULL;
   }
   if(ncplane_putchar_yx(non, ybase, 0, ' ') < 0 || ncplane_putchar(non, ' ') < 0){
@@ -160,7 +160,7 @@ outro_message(struct notcurses* nc, int* rows, int* cols){
   if(ncplane_set_bg_rgb8(non, 0, 180, 180)){
     return NULL;
   }
-  if(ncplane_set_bg_alpha(non, CELL_ALPHA_BLEND)){
+  if(ncplane_set_bg_alpha(non, NCALPHA_BLEND)){
     return NULL;
   }
   ncplane_on_styles(non, NCSTYLE_BOLD);
diff --git a/src/demo/reel.c b/src/demo/reel.c
index 3b00d864dd..59b2725bcc 100644
--- a/src/demo/reel.c
+++ b/src/demo/reel.c
@@ -219,11 +219,11 @@ ncreel_demo_core(struct notcurses* nc){
   ncchannels_set_fg_rgb8(&popts.borderchan, 136, 23, 152);
   ncchannels_set_bg_rgb8(&popts.borderchan, 0, 0, 0);
   uint64_t bgchannels = 0;
-  if(ncchannels_set_fg_alpha(&bgchannels, CELL_ALPHA_TRANSPARENT)){
+  if(ncchannels_set_fg_alpha(&bgchannels, NCALPHA_TRANSPARENT)){
     ncplane_destroy(n);
     return -1;
   }
-  if(ncchannels_set_bg_alpha(&bgchannels, CELL_ALPHA_TRANSPARENT)){
+  if(ncchannels_set_bg_alpha(&bgchannels, NCALPHA_TRANSPARENT)){
     ncplane_destroy(n);
     return -1;
   }
diff --git a/src/demo/trans.c b/src/demo/trans.c
index 0009a6f62d..793857c41e 100644
--- a/src/demo/trans.c
+++ b/src/demo/trans.c
@@ -41,8 +41,8 @@ legend(struct notcurses* nc, const char* msg){
   }
   nccell c = CELL_TRIVIAL_INITIALIZER;
   nccell_set_fg_rgb8(&c, 0, 0, 0); // darken surrounding characters by half
-  nccell_set_fg_alpha(&c, CELL_ALPHA_BLEND);
-  nccell_set_bg_alpha(&c, CELL_ALPHA_TRANSPARENT); // don't touch background
+  nccell_set_fg_alpha(&c, NCALPHA_BLEND);
+  nccell_set_bg_alpha(&c, NCALPHA_TRANSPARENT); // don't touch background
   if(ncplane_set_base_cell(n, &c)){
     ncplane_destroy(n);
     return NULL;
@@ -150,7 +150,7 @@ slidepanel(struct notcurses* nc, struct ncplane* stdn){
 
   // Next, we set our foreground transparent, allowing characters underneath to
   // be seen in their natural colors. Our background remains opaque+default.
-  nccell_set_fg_alpha(&c, CELL_ALPHA_TRANSPARENT);
+  nccell_set_fg_alpha(&c, NCALPHA_TRANSPARENT);
   ncplane_set_base_cell(n, &c);
   clock_gettime(CLOCK_MONOTONIC, &cur);
   deadlinens = timespec_to_ns(&cur) + timespec_to_ns(&demodelay);
@@ -165,7 +165,7 @@ slidepanel(struct notcurses* nc, struct ncplane* stdn){
   // Set the foreground color, setting it to blend. We should get the underlying
   // glyphs in a blended color, with the default background color.
   nccell_set_fg_rgb(&c, 0x80c080);
-  nccell_set_fg_alpha(&c, CELL_ALPHA_BLEND);
+  nccell_set_fg_alpha(&c, NCALPHA_BLEND);
   ncplane_set_base_cell(n, &c);
   clock_gettime(CLOCK_MONOTONIC, &cur);
   l = legend(nc, "default background, fg blended, no glyph");
@@ -180,7 +180,7 @@ slidepanel(struct notcurses* nc, struct ncplane* stdn){
   // Opaque foreground color. This produces underlying glyphs in the specified,
   // fixed color, with the default background color.
   nccell_set_fg_rgb(&c, 0x80c080);
-  nccell_set_fg_alpha(&c, CELL_ALPHA_OPAQUE);
+  nccell_set_fg_alpha(&c, NCALPHA_OPAQUE);
   ncplane_set_base_cell(n, &c);
   clock_gettime(CLOCK_MONOTONIC, &cur);
   l = legend(nc, "default background, fg colored opaque, no glyph");
@@ -196,8 +196,8 @@ slidepanel(struct notcurses* nc, struct ncplane* stdn){
   // Our background color remains opaque default.
   nccell_load_char(n, &c, 'X');
   nccell_set_fg_default(&c);
-  nccell_set_fg_alpha(&c, CELL_ALPHA_TRANSPARENT);
-  nccell_set_bg_alpha(&c, CELL_ALPHA_OPAQUE);
+  nccell_set_fg_alpha(&c, NCALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(&c, NCALPHA_OPAQUE);
   ncplane_set_base_cell(n, &c);
   clock_gettime(CLOCK_MONOTONIC, &cur);
   l = legend(nc, "default colors, fg transparent, print glyph");
@@ -211,8 +211,8 @@ slidepanel(struct notcurses* nc, struct ncplane* stdn){
 
   // Now we replace the characters with X's, but draw the foreground and
   // background color from below us.
-  nccell_set_fg_alpha(&c, CELL_ALPHA_TRANSPARENT);
-  nccell_set_bg_alpha(&c, CELL_ALPHA_TRANSPARENT);
+  nccell_set_fg_alpha(&c, NCALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(&c, NCALPHA_TRANSPARENT);
   ncplane_set_base_cell(n, &c);
   clock_gettime(CLOCK_MONOTONIC, &cur);
   l = legend(nc, "all transparent, print glyph");
@@ -226,8 +226,8 @@ slidepanel(struct notcurses* nc, struct ncplane* stdn){
 
   // Finally, we populate the plane for the first time with non-transparent
   // characters. We blend, however, to show the underlying color in our glyphs.
-  nccell_set_fg_alpha(&c, CELL_ALPHA_BLEND);
-  nccell_set_bg_alpha(&c, CELL_ALPHA_BLEND);
+  nccell_set_fg_alpha(&c, NCALPHA_BLEND);
+  nccell_set_bg_alpha(&c, NCALPHA_BLEND);
   nccell_set_fg_rgb(&c, 0x80c080);
   nccell_set_bg_rgb(&c, 0x204080);
   ncplane_set_base_cell(n, &c);
diff --git a/src/demo/unicodeblocks.c b/src/demo/unicodeblocks.c
index 8ad5fd1084..a73ed78086 100644
--- a/src/demo/unicodeblocks.c
+++ b/src/demo/unicodeblocks.c
@@ -72,10 +72,10 @@ draw_block(struct ncplane* nn, uint32_t blockstart){
   nccell ll = CELL_TRIVIAL_INITIALIZER, lr = CELL_TRIVIAL_INITIALIZER;
   nccell hl = CELL_TRIVIAL_INITIALIZER, vl = CELL_TRIVIAL_INITIALIZER;
   nccells_rounded_box(nn, 0, 0, &ul, &ur, &ll, &lr, &hl, &vl);
-  nccell_set_bg_alpha(&ul, CELL_ALPHA_TRANSPARENT);
-  nccell_set_bg_alpha(&ur, CELL_ALPHA_TRANSPARENT);
-  nccell_set_bg_alpha(&ll, CELL_ALPHA_TRANSPARENT);
-  nccell_set_bg_alpha(&lr, CELL_ALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(&ul, NCALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(&ur, NCALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(&ll, NCALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(&lr, NCALPHA_TRANSPARENT);
   nccell_set_fg_rgb8(&ul, 0xea, 0xaa, 0x00);
   nccell_set_fg_rgb8(&ur, 0x00, 0x30, 0x57);
   nccell_set_fg_rgb8(&ll, 0x00, 0x30, 0x57);
@@ -248,7 +248,7 @@ int unicodeblocks_demo(struct notcurses* nc){
     return -1;
   }
   uint64_t channels = 0;
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_BLEND);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_BLEND);
   ncchannels_set_fg_rgb(&channels, 0x004000);
   ncchannels_set_bg_rgb(&channels, 0x0);
   ncplane_set_base(header, "", 0, channels);
diff --git a/src/demo/view.c b/src/demo/view.c
index b2154c235a..4d73798401 100644
--- a/src/demo/view.c
+++ b/src/demo/view.c
@@ -70,13 +70,13 @@ legend(struct ncplane* stdn, int dimy, int dimx){
     .name = "lgd",
   };
   struct ncplane* n = ncplane_create(stdn, &nopts);
-  ncplane_set_bg_alpha(n, CELL_ALPHA_TRANSPARENT);
+  ncplane_set_bg_alpha(n, NCALPHA_TRANSPARENT);
   uint64_t channels = 0;
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
   ncplane_set_base(n, " ", 0, channels);
   ncplane_set_styles(n, NCSTYLE_BOLD);
   ncplane_set_fg_rgb8(n, 0xff, 0xff, 0xff);
-  ncplane_set_fg_alpha(n, CELL_ALPHA_HIGHCONTRAST);
+  ncplane_set_fg_alpha(n, NCALPHA_HIGHCONTRAST);
   if(ncplane_putstr_aligned(n, 0, NCALIGN_CENTER, "target launch") <= 0){
     ncplane_destroy(n);
     return NULL;
@@ -136,8 +136,8 @@ view_images(struct notcurses* nc, struct ncplane* nstd, int dimy, int dimx){
     return -1;
   }
   uint64_t channels = 0;
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
   ncplane_set_base(dsplane, "", 0, channels);
   ncvisual_destroy(ncv2);
   demo_render(nc);
diff --git a/src/demo/whiteout.c b/src/demo/whiteout.c
index 28cc24c0f5..e2397b6e3f 100644
--- a/src/demo/whiteout.c
+++ b/src/demo/whiteout.c
@@ -27,8 +27,8 @@ mathplane(struct notcurses* nc){
   struct ncplane* n = ncplane_create(stdn, &nopts);
   uint64_t channels = 0;
   ncchannels_set_fg_rgb(&channels, 0x2b50c8); // metallic gold, inverted
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_BLEND);
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_BLEND);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
   ncplane_set_base(n, "", 0, channels);
   ncplane_set_fg_rgb(n, 0xd4af37); // metallic gold
   ncplane_set_bg_rgb(n, 0x0);
@@ -168,8 +168,8 @@ static int
 message(struct ncplane* n, int maxy, int maxx, int num, int total,
         int bytes_out, int egs_out, int cols_out){
   uint64_t channels = 0;
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
   ncplane_set_base(n, "", 0, channels);
   ncplane_set_fg_rgb8(n, 255, 255, 255);
   ncplane_set_bg_rgb8(n, 32, 64, 32);
diff --git a/src/demo/xray.c b/src/demo/xray.c
index 67598a2cc0..70ea1309be 100644
--- a/src/demo/xray.c
+++ b/src/demo/xray.c
@@ -31,14 +31,14 @@ make_slider(struct notcurses* nc, int dimx){
   };
   struct ncplane* n = ncplane_create(notcurses_stdplane(nc), &nopts);
   uint64_t channels = 0;
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
   ncplane_set_base(n, " ", 0, channels);
   ncplane_set_scrolling(n, true);
   int r = 0x5f;
   int g = 0xaf;
   int b = 0x84;
-  ncplane_set_bg_alpha(n, CELL_ALPHA_TRANSPARENT);
+  ncplane_set_bg_alpha(n, NCALPHA_TRANSPARENT);
   for(int x = 0 ; x < REPS ; ++x){
     for(size_t l = 0 ; l < sizeof(leg) / sizeof(*leg) ; ++l){
       ncplane_set_fg_rgb8_clipped(n, r + 0x8 * l, g + 0x8 * l, b + 0x8 * l);
@@ -169,8 +169,8 @@ int xray_demo(struct notcurses* nc){
     return -1;
   }
   uint64_t stdc = 0;
-  ncchannels_set_fg_alpha(&stdc, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_bg_alpha(&stdc, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&stdc, NCALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&stdc, NCALPHA_TRANSPARENT);
   ncplane_set_base(notcurses_stdplane(nc), "", 0, stdc);
   // returns non-zero if the selected blitter isn't available
   if(notcurses_check_pixel_support(nc) < 1){
diff --git a/src/demo/yield.c b/src/demo/yield.c
index 9d49e533c8..22f81078fb 100644
--- a/src/demo/yield.c
+++ b/src/demo/yield.c
@@ -67,10 +67,10 @@ int yield_demo(struct notcurses* nc){
     return -1;
   }
   uint64_t basechan = 0;
-  ncchannels_set_bg_alpha(&basechan, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_fg_alpha(&basechan, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&basechan, NCALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&basechan, NCALPHA_TRANSPARENT);
   ncplane_set_base(label, "", 0, basechan);
-  ncplane_set_bg_alpha(label, CELL_ALPHA_TRANSPARENT);
+  ncplane_set_bg_alpha(label, NCALPHA_TRANSPARENT);
   ncplane_set_fg_rgb8(label, 0xff, 0xff, 0xff);
   ncplane_set_styles(label, NCSTYLE_BOLD);
   ncplane_printf_aligned(label, 0, NCALIGN_CENTER, "Yield: %03.1f%%", ((double)tfilled * 100) / threshold_painted);
diff --git a/src/demo/zoo.c b/src/demo/zoo.c
index ce79f69d99..885dbed686 100644
--- a/src/demo/zoo.c
+++ b/src/demo/zoo.c
@@ -75,8 +75,8 @@ multiselector_demo(struct ncplane* n, struct ncplane* under, int y){
     .titlechannels = CHANNELS_RGB_INITIALIZER(0x80, 0x80, 0xff, 0, 0, 0x20),
   };
   uint64_t bgchannels = CHANNELS_RGB_INITIALIZER(0, 0x40, 0, 0, 0x40, 0);
-  ncchannels_set_fg_alpha(&bgchannels, CELL_ALPHA_BLEND);
-  ncchannels_set_bg_alpha(&bgchannels, CELL_ALPHA_BLEND);
+  ncchannels_set_fg_alpha(&bgchannels, NCALPHA_BLEND);
+  ncchannels_set_bg_alpha(&bgchannels, NCALPHA_BLEND);
   struct ncplane_options nopts = {
     .y = y,
     .x = 0,
@@ -112,8 +112,8 @@ selector_demo(struct ncplane* n, struct ncplane* under, int dimx, int y){
     .titlechannels = CHANNELS_RGB_INITIALIZER(0xff, 0xff, 0x80, 0, 0, 0x20),
   };
   uint64_t bgchannels = CHANNELS_RGB_INITIALIZER(0, 0, 0x40, 0, 0, 0x40);
-  ncchannels_set_fg_alpha(&bgchannels, CELL_ALPHA_BLEND);
-  ncchannels_set_bg_alpha(&bgchannels, CELL_ALPHA_BLEND);
+  ncchannels_set_fg_alpha(&bgchannels, NCALPHA_BLEND);
+  ncchannels_set_bg_alpha(&bgchannels, NCALPHA_BLEND);
   struct ncplane_options nopts = {
     .y = y,
     .x = dimx,
@@ -392,8 +392,8 @@ reader_demo(struct notcurses* nc){
   }
   ncplane_set_fg_rgb8(rp, 0x20, 0xe0, 0xe0);
   uint64_t echannels = 0;
-  ncchannels_set_fg_alpha(&echannels, CELL_ALPHA_BLEND);
-  ncchannels_set_bg_alpha(&echannels, CELL_ALPHA_BLEND);
+  ncchannels_set_fg_alpha(&echannels, NCALPHA_BLEND);
+  ncchannels_set_bg_alpha(&echannels, NCALPHA_BLEND);
   ncplane_set_base(rp, "", 0, echannels);
   ncplane_set_scrolling(rp, true);
   // Bring the selector left across the top, while raising the exposition
diff --git a/src/fetch/main.c b/src/fetch/main.c
index 99ba33e32f..bcccb2dc91 100644
--- a/src/fetch/main.c
+++ b/src/fetch/main.c
@@ -578,7 +578,6 @@ display_thread(void* vmarshal){
   struct marshal* m = vmarshal;
   drawpalette(m->nc);
   if(ncdirect_canopen_images(m->nc)){
-    ncdirect_check_pixel_support(m->nc);
     if(m->logo){
       if(ncdirect_render_image(m->nc, m->logo, NCALIGN_CENTER,
                                NCBLIT_PIXEL, NCSCALE_SCALE_HIRES) == 0){
diff --git a/src/info/main.c b/src/info/main.c
index 239fe5be94..c79a1b728c 100644
--- a/src/info/main.c
+++ b/src/info/main.c
@@ -1,7 +1,35 @@
+#include <errno.h>
+#include <unistd.h>
 #include <stdlib.h>
 #include <notcurses/notcurses.h>
 
+// write(2) with retry on partial write or interrupted write
+static inline ssize_t
+writen(int fd, const void* buf, size_t len){
+  ssize_t r;
+  size_t w = 0;
+  while(w < len){
+    if((r = write(fd, (const char*)buf + w, len - w)) < 0){
+      if(errno == EAGAIN || errno == EBUSY || errno == EINTR){
+        continue;
+      }
+      return -1;
+    }
+    w += r;
+  }
+  return w;
+}
+
 int main(void){
+  char* mbuf = NULL;
+  size_t len = 0;
+  FILE* mstream;
+  if((mstream = open_memstream(&mbuf, &len)) == NULL){
+    return EXIT_FAILURE;
+  }
+  if(fprintf(mstream, "\n") != 1){
+    return EXIT_FAILURE;
+  }
   notcurses_options nopts = {
     .flags = NCOPTION_NO_ALTERNATE_SCREEN,
   };
@@ -9,7 +37,17 @@ int main(void){
   if(nc == NULL){
     return EXIT_FAILURE;
   }
-  notcurses_check_pixel_support(nc);
-  notcurses_debug_caps(nc, stdout);
+  notcurses_debug_caps(nc, mstream);
+  if(fclose(mstream)){
+    notcurses_stop(nc);
+    fprintf(stderr, "Error closing memstream after %zuB\n", len);
+    return EXIT_FAILURE;
+  }
+  if(writen(fileno(stdout), mbuf, len) < 0){
+    notcurses_stop(nc);
+    fprintf(stderr, "Error writing %zuB memstream\n", len);
+    return EXIT_FAILURE;
+  }
+  free(mbuf);
   return notcurses_stop(nc) ? EXIT_FAILURE : EXIT_SUCCESS;
 }
diff --git a/src/lib/blit.c b/src/lib/blit.c
index e7160e726e..0763571030 100644
--- a/src/lib/blit.c
+++ b/src/lib/blit.c
@@ -71,12 +71,12 @@ tria_blit_ascii(ncplane* nc, int linesize, const void* data,
       c->channels = 0;
       c->stylemask = 0;
       if(blendcolors){
-        nccell_set_bg_alpha(c, CELL_ALPHA_BLEND);
-        nccell_set_fg_alpha(c, CELL_ALPHA_BLEND);
+        nccell_set_bg_alpha(c, NCALPHA_BLEND);
+        nccell_set_fg_alpha(c, NCALPHA_BLEND);
       }
       if(rgba_trans_q(rgbbase_up, bargs->transcolor)){
-        nccell_set_bg_alpha(c, CELL_ALPHA_TRANSPARENT);
-        nccell_set_fg_alpha(c, CELL_ALPHA_TRANSPARENT);
+        nccell_set_bg_alpha(c, NCALPHA_TRANSPARENT);
+        nccell_set_fg_alpha(c, NCALPHA_TRANSPARENT);
         cell_set_blitquadrants(c, 0, 0, 0, 0);
       }else{
         nccell_set_fg_rgb8(c, rgbbase_up[0], rgbbase_up[1], rgbbase_up[2]);
@@ -130,13 +130,13 @@ tria_blit(ncplane* nc, int linesize, const void* data, int leny, int lenx,
       c->channels = 0;
       c->stylemask = 0;
       if(blendcolors){
-        nccell_set_bg_alpha(c, CELL_ALPHA_BLEND);
-        nccell_set_fg_alpha(c, CELL_ALPHA_BLEND);
+        nccell_set_bg_alpha(c, NCALPHA_BLEND);
+        nccell_set_fg_alpha(c, NCALPHA_BLEND);
       }
       if(rgba_trans_q(rgbbase_up, transcolor) || rgba_trans_q(rgbbase_down, transcolor)){
-        nccell_set_bg_alpha(c, CELL_ALPHA_TRANSPARENT);
+        nccell_set_bg_alpha(c, NCALPHA_TRANSPARENT);
         if(rgba_trans_q(rgbbase_up, transcolor) && rgba_trans_q(rgbbase_down, transcolor)){
-          nccell_set_fg_alpha(c, CELL_ALPHA_TRANSPARENT);
+          nccell_set_fg_alpha(c, NCALPHA_TRANSPARENT);
         }else if(rgba_trans_q(rgbbase_up, transcolor)){ // down has the color
           if(pool_blit_direct(&nc->pool, c, "\u2584", strlen("\u2584"), 1) <= 0){
             return -1;
@@ -412,11 +412,11 @@ qtrans_check(nccell* c, unsigned blendcolors,
     }
   }
   assert(egc);
-  nccell_set_bg_alpha(c, CELL_ALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(c, NCALPHA_TRANSPARENT);
   if(*egc == '\0'){
-    nccell_set_fg_alpha(c, CELL_ALPHA_TRANSPARENT);
+    nccell_set_fg_alpha(c, NCALPHA_TRANSPARENT);
   }else if(blendcolors){
-    nccell_set_fg_alpha(c, CELL_ALPHA_BLEND);
+    nccell_set_fg_alpha(c, NCALPHA_BLEND);
   }
 //fprintf(stderr, "QBQ: 0x%x\n", cell_blittedquadrants(c));
   return egc;
@@ -479,8 +479,8 @@ quadrant_blit(ncplane* nc, int linesize, const void* data, int leny, int lenx,
         cell_set_fchannel(c, fg);
         cell_set_bchannel(c, bg);
         if(blendcolors){
-          nccell_set_bg_alpha(c, CELL_ALPHA_BLEND);
-          nccell_set_fg_alpha(c, CELL_ALPHA_BLEND);
+          nccell_set_bg_alpha(c, NCALPHA_BLEND);
+          nccell_set_fg_alpha(c, NCALPHA_BLEND);
         }
         cell_set_blitquadrants(c, 1, 1, 1, 1);
       }
@@ -590,8 +590,8 @@ sex_solver(const uint32_t rgbas[6], uint64_t* channels, unsigned blendcolors){
 //fprintf(stderr, "solved for best: %d (%u)\n", best, mindiff);
   assert(best >= 0 && best < 32);
   if(blendcolors){
-    ncchannels_set_fg_alpha(channels, CELL_ALPHA_BLEND);
-    ncchannels_set_bg_alpha(channels, CELL_ALPHA_BLEND);
+    ncchannels_set_fg_alpha(channels, NCALPHA_BLEND);
+    ncchannels_set_bg_alpha(channels, NCALPHA_BLEND);
   }
   return sex[best];
 }
@@ -629,20 +629,20 @@ sex_trans_check(cell* c, const uint32_t rgbas[6], unsigned blendcolors,
   if(transstring == 0){ // there was no transparency
     return NULL;
   }
-  nccell_set_bg_alpha(c, CELL_ALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(c, NCALPHA_TRANSPARENT);
   // there were some transparent pixels. since they get priority, the foreground
   // is just a general lerp across non-transparent pixels.
   const char* egc = sex[transstring];
-  nccell_set_bg_alpha(c, CELL_ALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(c, NCALPHA_TRANSPARENT);
 //fprintf(stderr, "transtring: %u egc: %s\n", transtring, egc);
   if(*egc == ' '){ // entirely transparent
-    nccell_set_fg_alpha(c, CELL_ALPHA_TRANSPARENT);
+    nccell_set_fg_alpha(c, NCALPHA_TRANSPARENT);
     return "";
   }else{ // partially transparent, thus div >= 1
 //fprintf(stderr, "div: %u r: %u g: %u b: %u\n", div, r, g, b);
     cell_set_fchannel(c, generalerp(r, g, b, div));
     if(blendcolors){
-      nccell_set_fg_alpha(c, CELL_ALPHA_BLEND);
+      nccell_set_fg_alpha(c, NCALPHA_BLEND);
     }
     cell_set_blitquadrants(c, !(transstring & 5u), !(transstring & 10u),
                               !(transstring & 20u), !(transstring & 40u));
@@ -821,14 +821,14 @@ braille_blit(ncplane* nc, int linesize, const void* data, int leny, int lenx,
       c->channels = 0;
       c->stylemask = 0;
       if(blendcolors){
-        nccell_set_fg_alpha(c, CELL_ALPHA_BLEND);
+        nccell_set_fg_alpha(c, NCALPHA_BLEND);
       }
       // FIXME for now, we just sample, color-wise, and always draw crap.
       // more complicated to do optimally than quadrants, for sure. ideally,
       // we only get one color in an area.
-      nccell_set_bg_alpha(c, CELL_ALPHA_TRANSPARENT);
+      nccell_set_bg_alpha(c, NCALPHA_TRANSPARENT);
       if(!egcidx){
-          nccell_set_fg_alpha(c, CELL_ALPHA_TRANSPARENT);
+          nccell_set_fg_alpha(c, NCALPHA_TRANSPARENT);
           // FIXME else look for pairs of transparency!
       }else{
         if(blends){
diff --git a/src/lib/debug.c b/src/lib/debug.c
index 8a71c9cdfa..b8e55042e6 100644
--- a/src/lib/debug.c
+++ b/src/lib/debug.c
@@ -26,41 +26,42 @@ tinfo_debug_caps(const tinfo* ti, FILE* debugfp, int rows, int cols,
                   capyn(get_escape(ti, ESCAPE_BGOP)));
   fprintf(debugfp, "%srows: %u cols: %u rpx: %u cpx: %u (%dx%d)\n",
           indent, rows, cols, ti->cellpixy, ti->cellpixx, rows * ti->cellpixy, cols * ti->cellpixx);
-  if(!ti->pixel_query_done){
-    fprintf(debugfp, "%sno bitmap graphics information yet\n", indent);
+  if(!ti->bitmap_supported){
+    fprintf(debugfp, "%sdidn't detect bitmap graphics support\n", indent);
+  }else if(ti->sixel_maxy){
+    fprintf(debugfp, "%smax sixel size: %dx%d colorregs: %u\n",
+            indent, ti->sixel_maxy, ti->sixel_maxx, ti->color_registers);
+  }else if(ti->color_registers){
+    fprintf(debugfp, "%ssixel colorregs: %u\n", indent, ti->color_registers);
   }else{
-    if(!ti->bitmap_supported){
-      fprintf(debugfp, "%sdidn't detect bitmap graphics support\n", indent);
-    }else if(ti->sixel_maxy || ti->color_registers){
-      fprintf(debugfp, "%smax sixel size: %dx%d colorregs: %u\n",
-              indent, ti->sixel_maxy, ti->sixel_maxx, ti->color_registers);
-    }else{
-      fprintf(debugfp, "%sRGBA pixel graphics supported\n", indent);
-    }
+    fprintf(debugfp, "%srgba pixel graphics supported\n", indent);
   }
-  fprintf(debugfp, "%sUTF8: %c quad: %c sex: %c braille: %c images: %c videos: %c\n",
+  fprintf(debugfp, "%sutf8: %c quad: %c sex: %c braille: %c images: %c videos: %c\n",
           indent, capbool(ti->utf8), capbool(ti->quadrants),
           capbool(ti->sextants), capbool(ti->braille),
           capbool(images), capbool(videos));
   if(ti->utf8){
-    fprintf(debugfp, "%s  Halves {%ls}   Quads {%ls} Light ⎧%.6ls%.3ls⎫ Heavy ⎧%.6ls%.3ls⎫\n", indent,
+    fprintf(debugfp, "%s  halves {%ls}   quads {%ls} light ⎧%.6ls%.3ls⎫ heavy ⎧%.6ls%.3ls⎫ ⎧█ ⎫ 🯰🯱\n", indent,
             get_blitter_egcs(NCBLIT_2x1), get_blitter_egcs(NCBLIT_2x2),
             NCBOXLIGHTW, NCBOXLIGHTW + 4,
             NCBOXHEAVYW, NCBOXHEAVYW + 4);
-    fprintf(debugfp, "%sSextants ⎧%.120ls⎫       ⎩%.6ls%.3ls⎭       ⎩%.6ls%.3ls⎭\n", indent,
+    fprintf(debugfp, "%ssextants ⎧%.120ls⎫       ⎩%.6ls%.3ls⎭       ⎩%.6ls%.3ls⎭ ⎪🮋▏⎪ 🯲🯳\n", indent,
             get_blitter_egcs(NCBLIT_3x2),
             NCBOXLIGHTW + 2, NCBOXLIGHTW + 5,
             NCBOXHEAVYW + 2, NCBOXHEAVYW + 5);
-    fprintf(debugfp, "%s         ⎩%ls⎭ Round ⎧%.6ls%.3ls⎫ Frame ⎧%.6ls%.3ls⎫\n", indent,
+    fprintf(debugfp, "%s         ⎩%ls⎭ round ⎧%.6ls%.3ls⎫ frame ⎧%.6ls%.3ls⎫ ⎪🮊▎⎪ 🯴🯵\n", indent,
             get_blitter_egcs(NCBLIT_3x2) + 32,
             NCBOXROUNDW, NCBOXROUNDW + 4,
             NCBOXDOUBLEW, NCBOXDOUBLEW + 4);
-    fprintf(debugfp, "%s Braille {%ls}             ⎩%.6ls%.3ls⎭       ⎩%.6ls%.3ls⎭\n", indent,
+    fprintf(debugfp, "%s braille ⎡%.120ls⎤             ⎩%.6ls%.3ls⎭       ⎩%.6ls%.3ls⎭ ⎪🮉▍⎪ 🯶🯷\n", indent,
             get_blitter_egcs(NCBLIT_BRAILLE),
             NCBOXROUNDW + 2, NCBOXROUNDW + 5,
             NCBOXDOUBLEW + 2, NCBOXDOUBLEW + 5);
-    fprintf(debugfp, "%sLower ⅛s {%ls} Upper ⅛s {%s}\n", indent,
-            get_blitter_egcs(NCBLIT_8x1), " ▔🮂🮃▀🮄🮅🮆█");
+    fprintf(debugfp, "%s         ⎢%ls⎥                               ⎨▐▌⎬ 🯸🯹\n", indent, get_blitter_egcs(NCBLIT_BRAILLE)); // FIXME
+    fprintf(debugfp, "%s         ⎢%ls⎥                               ⎪🮈▋⎪\n", indent, get_blitter_egcs(NCBLIT_BRAILLE)); // FIXME
+    fprintf(debugfp, "%s         ⎣%ls⎦                               ⎪🮇▊⎪\n", indent, get_blitter_egcs(NCBLIT_BRAILLE)); // FIXME
+    fprintf(debugfp, "%s vert ⅛s ⎛%ls⎞ ▔🭶🭷🭸🭹🭺🭻▁                                      ⎪▕▉⎪\n", indent, get_blitter_egcs(NCBLIT_8x1));
+    fprintf(debugfp, "%s         ⎝%s⎠                                               ⎩ █⎭\n", indent, "█🮆🮅🮄▀🮃🮂▔ ");
   }
   if(ti->bg_collides_default){
     fprintf(debugfp, "%sbackground of 0x%06lx is considered transparent\n", indent, ti->bg_collides_default & 0xfffffful);
diff --git a/src/lib/direct.c b/src/lib/direct.c
index 6fab2df145..5cae3eb8e2 100644
--- a/src/lib/direct.c
+++ b/src/lib/direct.c
@@ -495,12 +495,12 @@ ncdirect_dump_plane(ncdirect* n, const ncplane* np, int xoff){
       if(egc == NULL){
         return -1;
       }
-      if(ncchannels_fg_alpha(channels) == CELL_ALPHA_TRANSPARENT){
+      if(ncchannels_fg_alpha(channels) == NCALPHA_TRANSPARENT){
         ncdirect_set_fg_default(n);
       }else{
         ncdirect_set_fg_rgb(n, ncchannels_fg_rgb(channels));
       }
-      if(ncchannels_bg_alpha(channels) == CELL_ALPHA_TRANSPARENT){
+      if(ncchannels_bg_alpha(channels) == NCALPHA_TRANSPARENT){
         ncdirect_set_bg_default(n);
       }else{
         ncdirect_set_bg_rgb(n, ncchannels_bg_rgb(channels));
@@ -832,7 +832,6 @@ int ncdirect_stop(ncdirect* nc){
   int ret = 0;
   if(nc){
     ret |= ncdirect_stop_minimal(nc);
-    input_free_esctrie(&nc->tcache.input.inputescapes);
     free(nc);
   }
   return ret;
@@ -1278,9 +1277,6 @@ int ncdirect_flush(const ncdirect* nc){
 }
 
 int ncdirect_check_pixel_support(ncdirect* n){
-  if(query_term(&n->tcache, n->ctermfd)){
-    return -1;
-  }
   if(n->tcache.bitmap_supported){
     return 1;
   }
diff --git a/src/lib/input.c b/src/lib/input.c
index 65c99dcd43..9d7245d5f1 100644
--- a/src/lib/input.c
+++ b/src/lib/input.c
@@ -115,7 +115,8 @@ create_esctrie_node(int special){
   return e;
 }
 
-void input_free_esctrie(esctrie** eptr){
+static void
+input_free_esctrie(esctrie** eptr){
   esctrie* e;
   if( (e = *eptr) ){
     if(e->trie){
@@ -387,14 +388,8 @@ handle_queued_input(ncinputlayer* nc, ncinput* ni, int leftmargin, int topmargin
 static char32_t
 handle_input(ncinputlayer* nc, ncinput* ni, int leftmargin, int topmargin,
              const sigset_t* sigmask){
-  // we once used getc() here (and kept ttyinfp, a FILE*, instead of the file
-  // descriptor ttyinfd), but under tmux, the first time running getc() would
-  // (bewilderingly) see a series of terminal reset codes emitted. this has
-  // never been explained to my satisfaction, but we can work around it by
-  // using a lower-level read() anyway.
-  // see https://github.com/dankamongmen/notcurses/issues/1314 for more info.
   unsigned char c;
-  while(!input_queue_full(nc) && read(nc->ttyinfd, &c, 1) > 0){
+  while(!input_queue_full(nc) && read(nc->infd, &c, 1) > 0){
     nc->inputbuf[nc->inputbuf_write_at] = c;
 //fprintf(stderr, "OCCUPY: %u@%u read: %d\n", nc->inputbuf_occupied, nc->inputbuf_write_at, nc->inputbuf[nc->inputbuf_write_at]);
     if(++nc->inputbuf_write_at == sizeof(nc->inputbuf) / sizeof(*nc->inputbuf)){
@@ -402,7 +397,7 @@ handle_input(ncinputlayer* nc, ncinput* ni, int leftmargin, int topmargin,
     }
     ++nc->inputbuf_occupied;
     const struct timespec ts = {};
-    if(block_on_input(nc->ttyinfd, &ts, sigmask) < 1){
+    if(block_on_input(nc->infd, &ts, sigmask) < 1){
       break;
     }
   }
@@ -456,7 +451,7 @@ ncinputlayer_prestamp(ncinputlayer* nc, const struct timespec *ts,
     return handle_queued_input(nc, ni, leftmargin, topmargin);
   }
   errno = 0;
-  if(block_on_input(nc->ttyinfd, ts, sigmask) > 0){
+  if(block_on_input(nc->infd, ts, sigmask) > 0){
 //fprintf(stderr, "%d events from input!\n", events);
     return handle_ncinput(nc, ni, leftmargin, topmargin, sigmask);
   }
@@ -490,7 +485,9 @@ char32_t ncdirect_getc(ncdirect* nc, const struct timespec *ts,
   return r;
 }
 
-int prep_special_keys(ncinputlayer* nc){
+// load all known special keys from terminfo, and build the input sequence trie
+static int
+prep_special_keys(ncinputlayer* nc){
   static const struct {
     const char* tinfo;
     char32_t key;
@@ -605,3 +602,330 @@ int prep_special_keys(ncinputlayer* nc){
   }
   return 0;
 }
+
+void ncinputlayer_stop(ncinputlayer* nilayer){
+  if(nilayer->ttyfd >= 0){
+    close(nilayer->ttyfd);
+  }
+  input_free_esctrie(&nilayer->inputescapes);
+}
+
+typedef struct init_state {
+  tinfo* tcache;
+  enum {
+    STATE_NULL,
+    STATE_ESC,  // escape; aborts any active sequence
+    STATE_CSI,  // control sequence introducer
+    STATE_DCS,  // device control string
+    // XTVERSION replies with DCS > | ... ST
+    STATE_XTVERSION1,
+    STATE_XTVERSION2,
+    // XTGETTCAP replies with DCS 1 + r for a good request, or 0 + r for bad
+    STATE_XTGETTCAP1, // XTGETTCAP, got '0/1' (DCS 0/1 + r Pt ST)
+    STATE_XTGETTCAP2, // XTGETTCAP, got '+' (DCS 0/1 + r Pt ST)
+    STATE_XTGETTCAP3, // XTGETTCAP, got 'r' (DCS 0/1 + r Pt ST)
+    STATE_DCS_DRAIN,  // throw away input until we hit escape
+    STATE_TDA,  // tertiary DA
+    STATE_SDA,  // secondary DA (CSI > Pp ; Pv ; Pc c)
+    STATE_DA,   // primary DA   (CSI ? ... c) OR XTSMGRAPHICS
+    STATE_DA_1, // got '1', XTSMGRAPHICS color registers or primary DA
+    STATE_DA_1_SEMI, // got '1;'
+    STATE_DA_1_0, // got '1;0', XTSMGRAPHICS color registers or VT101
+    STATE_DA_6, // got '6', could be VT102 or VT220/VT320/VT420
+    STATE_DA_DRAIN, // drain out the primary DA to 'c'
+    STATE_SIXEL,// XTSMGRAPHICS about Sixel geometry (got '2')
+    STATE_SIXEL_SEMI1,   // got first semicolon in sixel geometry, want Ps
+    STATE_SIXEL_SUCCESS, // got Ps == 0, want second semicolon
+    STATE_SIXEL_WIDTH,   // reading maximum sixel width until ';'
+    STATE_SIXEL_HEIGHT,  // reading maximum sixel height until 'S'
+    STATE_SIXEL_CREGS,   // reading max color registers until 'S'
+    STATE_XTSMGRAPHICS_DRAIN, // drain out XTSMGRAPHICS to 'S'
+  } state;
+  int numeric;          // currently-lexed numeric
+  bool xtgettcap_good;  // high when we've received DCS 1
+} init_state;
+
+static int
+ruts_numeric(int* numeric, unsigned char c){
+  if(!isdigit(c)){
+    return -1;
+  }
+  int digit = c - '0';
+  if(INT_MAX / 10 - digit < *numeric){ // would overflow
+    return -1;
+  }
+  *numeric *= 10;
+  *numeric += digit;
+  return 0;
+}
+
+// FIXME ought implement the full Williams automaton
+// FIXME doesn't handle 8-bit controls (would need convert UTF-8)
+// returns 1 after handling the Device Attributes response, 0 if more input
+// ought be fed to the machine, and -1 on an invalid state transition.
+static int
+pump_control_read(init_state* inits, unsigned char c){
+//fprintf(stderr, "state: %2d char: %1c %3d %02x\n", inits->state, isprint(c) ? c : ' ', c, c);
+  if(c == NCKEY_ESC){
+    /*if(inits->state != STATE_NULL && inits->state != STATE_DCS && inits->state != STATE_DCS_DRAIN && inits->state != STATE_XTVERSION2 && inits->state != STATE_XTGETTCAP3 && inits->state != STATE_DA_DRAIN){
+      fprintf(stderr, "Unexpected escape in state %d\n", inits->state);
+    }*/
+    inits->state = STATE_ESC;
+    return 0;
+  }
+  switch(inits->state){
+    case STATE_NULL:
+      // not an escape -- throw into user queue
+      break;
+    case STATE_ESC:
+      inits->numeric = 0;
+      if(c == '['){
+        inits->state = STATE_CSI;
+      }else if(c == 'P'){
+        inits->state = STATE_DCS;
+      }else if(c == '\\'){
+//fprintf(stderr, "string terminator -- parse previous response FIXME\n");
+        // FIXME only now do we parse e.g. XTGETTCAP response
+        inits->state = STATE_NULL;
+      }
+      break;
+    case STATE_CSI: // terminated by 0x40--0x7E ('@'--'~')
+      if(c == '?'){
+        inits->state = STATE_DA;
+      }else if(c == '>'){
+        inits->state = STATE_SDA;
+      }else if(c >= 0x40 && c <= 0x7E){
+        inits->state = STATE_NULL;
+      }
+      break;
+    case STATE_DCS: // terminated by ST
+      if(c == '\\'){
+        fprintf(stderr, "terminated DCS\n");
+        inits->state = STATE_NULL;
+      }else if(c == '1'){
+        inits->state = STATE_XTGETTCAP1;
+        inits->xtgettcap_good = true;
+      }else if(c == '0'){
+        inits->state = STATE_XTGETTCAP1;
+        inits->xtgettcap_good = false;
+      }else if(c == '>'){
+        inits->state = STATE_XTVERSION1;
+      }else{
+        inits->state = STATE_DCS_DRAIN;
+      }
+      break;
+    case STATE_DCS_DRAIN:
+      // we drain to ST, which is an escape, and thus already handled, so...
+      break;
+    case STATE_XTVERSION1:
+      if(c == '|'){
+        inits->state = STATE_XTVERSION2;
+      }else{
+        // FIXME error?
+      }
+      break;
+    case STATE_XTVERSION2:
+      // FIXME roll up string
+      break;
+    case STATE_XTGETTCAP1:
+      if(c == '+'){
+        inits->state = STATE_XTGETTCAP2;
+      }else{
+        // FIXME malformed
+      }
+      break;
+    case STATE_XTGETTCAP2:
+      if(c == 'r'){
+        inits->state = STATE_XTGETTCAP3;
+      }else{
+        // FIXME malformed
+      }
+      break;
+    case STATE_XTGETTCAP3:
+      // FIXME feed to terminfo buffer
+      break;
+    case STATE_TDA:
+      // FIXME
+      break;
+    case STATE_SDA:
+      if(c == 'c'){
+        inits->state = STATE_NULL;
+      }
+      break;
+    // primary device attributes and XTSMGRAPHICS replies are generally
+    // indistinguishable until well into the escape. one can get:
+    // XTSMGRAPHICS: CSI ? Pi ; Ps ; Pv S {Pi: 123} {Ps: 0123}
+    // DA: CSI ? 1 ; 2 c  ("VT100 with Advanced Video Option")
+    //     CSI ? 1 ; 0 c  ("VT101 with No Options")
+    //     CSI ? 4 ; 6 c  ("VT132 with Advanced Video and Graphics")
+    //     CSI ? 6 c  ("VT102")
+    //     CSI ? 7 c  ("VT131")
+    //     CSI ? 1 2 ; Ps c  ("VT125")
+    //     CSI ? 6 2 ; Ps c  ("VT220")
+    //     CSI ? 6 3 ; Ps c  ("VT320")
+    //     CSI ? 6 4 ; Ps c  ("VT420")
+    case STATE_DA: // return success on end of DA
+      if(c == '1'){
+        inits->state = STATE_DA_1;
+      }else if(c == '2'){
+        inits->state = STATE_SIXEL;
+      }else if(c == '4' || c == '7'){ // VT132, VT131
+        inits->state = STATE_DA_DRAIN;
+      }else if(c == '6'){
+        inits->state = STATE_DA_6;
+      }else if(c == 'c'){
+        inits->state = STATE_NULL;
+        return 1;
+      }
+      break;
+    case STATE_DA_1:
+      if(c == 'c'){
+        inits->state = STATE_NULL;
+        return 1;
+      }else if(c == ';'){
+        inits->state = STATE_DA_1_SEMI;
+      }else{
+        // FIXME error?
+      }
+      break;
+    case STATE_DA_1_SEMI:
+      if(c == '2'){ // VT100 with Advanced Video Option
+        inits->state = STATE_DA_DRAIN;
+      }else if(c == '0'){
+        inits->state = STATE_DA_1_0;
+      }
+      break;
+    case STATE_DA_1_0:
+      if(c == 'c'){ // VT101 with No Options
+        inits->state = STATE_NULL;
+      }else if(c == ';'){
+        inits->state = STATE_SIXEL_CREGS;
+      }else{
+        // FIXME error?
+      }
+      break;
+    case STATE_SIXEL_CREGS:
+      if(c == 'S'){
+        inits->tcache->color_registers = inits->numeric;
+        inits->state = STATE_NULL;
+      }else if(ruts_numeric(&inits->numeric, c)){
+        return -1;
+      }
+      break;
+    case STATE_DA_6:
+      if(c == 'c'){
+        inits->state = STATE_NULL;
+        return 1;
+      }
+      // FIXME
+      break;
+    case STATE_DA_DRAIN:
+      if(c == 'c'){
+        inits->state = STATE_NULL;
+        return 1;
+      }
+      break;
+    case STATE_SIXEL:
+      if(c == ';'){
+        inits->state = STATE_SIXEL_SEMI1;
+      }else{
+        // FIXME error?
+      }
+      break;
+    case STATE_SIXEL_SEMI1:
+      if(c == '0'){
+        inits->state = STATE_SIXEL_SUCCESS;
+      }else if(c == '2'){
+        inits->state = STATE_XTSMGRAPHICS_DRAIN;
+      }else{
+        // FIXME error?
+      }
+      break;
+    case STATE_SIXEL_SUCCESS:
+      if(c == ';'){
+        inits->state = STATE_SIXEL_WIDTH;
+      }else{
+        // FIXME error?
+      }
+      break;
+    case STATE_SIXEL_WIDTH:
+      if(c == ';'){
+        inits->tcache->sixel_maxx = inits->numeric;
+        inits->state = STATE_SIXEL_HEIGHT;
+        inits->numeric = 0;
+      }else if(ruts_numeric(&inits->numeric, c)){
+        return -1;
+      }
+      break;
+    case STATE_SIXEL_HEIGHT:
+      if(c == 'S'){
+        inits->tcache->sixel_maxy = inits->numeric;
+        inits->state = STATE_NULL;
+      }else if(ruts_numeric(&inits->numeric, c)){
+        return -1;
+      }
+      break;
+    case STATE_XTSMGRAPHICS_DRAIN:
+      if(c == 'S'){
+        inits->state = STATE_NULL;
+      }
+      break;
+    default:
+      fprintf(stderr, "Reached invalid init state %d\n", inits->state);
+      return -1;
+  }
+  return 0;
+}
+
+// complete the terminal detection process
+static int
+control_read(tinfo* tcache, int ttyfd){
+  init_state inits = {
+    .tcache = tcache,
+    .state = STATE_NULL,
+  };
+  unsigned char* buf;
+  ssize_t s;
+
+  if((buf = malloc(BUFSIZ)) == NULL){
+    return -1;
+  }
+  while((s = read(ttyfd, buf, sizeof(buf))) != -1){
+    for(ssize_t idx = 0; idx < s ; ++idx){
+      int r = pump_control_read(&inits, buf[idx]);
+      if(r == 1){ // success!
+        free(buf);
+        return 0;
+      }else if(r < 0){
+        goto err;
+      }
+    }
+  }
+err:
+  fprintf(stderr, "Reading control replies failed on %d (%s)\n", ttyfd, strerror(errno));
+  free(buf);
+  return -1;
+}
+
+int ncinputlayer_init(tinfo* tcache, FILE* infp){
+  ncinputlayer* nilayer = &tcache->input;
+  setbuffer(infp, NULL, 0);
+  nilayer->inputescapes = NULL;
+  nilayer->infd = fileno(infp);
+  nilayer->ttyfd = isatty(nilayer->infd) ? -1 : get_tty_fd(NULL, infp);
+  if(prep_special_keys(nilayer)){
+    return -1;
+  }
+  nilayer->inputbuf_occupied = 0;
+  nilayer->inputbuf_valid_starts = 0;
+  nilayer->inputbuf_write_at = 0;
+  nilayer->input_events = 0;
+  int csifd = nilayer->ttyfd >= 0 ? nilayer->ttyfd : nilayer->infd;
+  if(isatty(csifd)){
+    if(control_read(tcache, csifd)){
+      input_free_esctrie(&nilayer->inputescapes);
+      return -1;
+    }
+  }
+  return 0;
+}
diff --git a/src/lib/internal.h b/src/lib/internal.h
index 6ba3275953..f9c5099e71 100644
--- a/src/lib/internal.h
+++ b/src/lib/internal.h
@@ -23,6 +23,7 @@ extern "C" {
 #include <termios.h>
 #include <stdbool.h>
 #include <unictype.h>
+#include <sys/poll.h>
 #include <langinfo.h>
 #include <netinet/in.h>
 #include "notcurses/notcurses.h"
@@ -33,7 +34,6 @@ extern "C" {
 #define API __attribute__((visibility("default")))
 #define ALLOC __attribute__((malloc)) __attribute__((warn_unused_result))
 
-struct esctrie;
 struct sixelmap;
 struct ncvisual_details;
 
@@ -435,7 +435,7 @@ struct crender {
     // take this into account when solving the background color.
     unsigned blittedquads: 4;
     unsigned damaged: 1; // only used in rasterization
-    // if CELL_ALPHA_HIGHCONTRAST is in play, we apply the HSV flip once the
+    // if NCALPHA_HIGHCONTRAST is in play, we apply the HSV flip once the
     // background is locked in. set highcontrast to indicate this.
     unsigned highcontrast: 1;
     unsigned fgblends: 8;
@@ -605,11 +605,6 @@ int reset_term_attributes(const tinfo* ti, FILE* fp);
 
 void free_terminfo_cache(tinfo* ti);
 
-// perform queries that require writing to the terminal, and reading a
-// response, rather than simply reading the terminfo database. can result
-// in a lengthy delay or even block if the terminal doesn't respond.
-int query_term(tinfo* ti, int fd);
-
 // if there were missing elements we wanted from terminfo, bitch about them here
 void warn_terminfo(const notcurses* nc, const tinfo* ti);
 
@@ -635,12 +630,6 @@ ncplane_stdplane_const(const ncplane* n){
   return notcurses_stdplane_const(ncplane_notcurses_const(n));
 }
 
-// load all known special keys from terminfo, and build the input sequence trie
-int prep_special_keys(ncinputlayer* nc);
-
-// free up the input escapes trie
-void input_free_esctrie(struct esctrie** trie);
-
 // initialize libav
 int ncvisual_init(int loglevel);
 
@@ -955,7 +944,7 @@ clamp_to_sixelmax(const tinfo* t, int* y, int* x, int* outy, ncscale_e scaling){
   *outy = *y;
   if(*outy % t->sprixel_scale_height){
     *outy += t->sprixel_scale_height - (*outy % t->sprixel_scale_height);
-    while(*outy > t->sixel_maxy){
+    while(t->sixel_maxy && *outy > t->sixel_maxy){
       *outy -= t->sprixel_scale_height;
     }
     if(scaling == NCSCALE_STRETCH || *y > *outy){
@@ -1336,7 +1325,7 @@ cell_set_fchannel(nccell* cl, uint32_t channel){
 // Palette-indexed colors do not blend. Do not pass me palette-indexed channels!
 static inline unsigned
 channels_blend(unsigned c1, unsigned c2, unsigned* blends){
-  if(ncchannel_alpha(c2) == CELL_ALPHA_TRANSPARENT){
+  if(ncchannel_alpha(c2) == NCALPHA_TRANSPARENT){
     return c1; // do *not* increment *blends
   }
   bool c2default = ncchannel_default_p(c2);
@@ -1504,7 +1493,13 @@ cellcmp_and_dupfar(egcpool* dampool, nccell* damcell,
   return 1;
 }
 
-int ncinputlayer_init(ncinputlayer* nilayer, FILE* infp);
+// sets up the input layer, building a trie of escape sequences and their
+// nckey equivalents. if we are connected to a tty, this also completes the
+// terminal detection sequence (we ought have already written our initial
+// queries, ideally as early as possible).
+int ncinputlayer_init(tinfo* tcache, FILE* infp);
+
+void ncinputlayer_stop(ncinputlayer* nilayer);
 
 // FIXME absorb into ncinputlayer_init()
 int cbreak_mode(int ttyfd, const struct termios* tpreserved);
diff --git a/src/lib/menu.c b/src/lib/menu.c
index 750b7891a7..a2a12aba35 100644
--- a/src/lib/menu.c
+++ b/src/lib/menu.c
@@ -383,8 +383,8 @@ ncmenu* ncmenu_create(ncplane* n, const ncmenu_options* opts){
         ret->disablechannels = ret->sectionchannels;
         ncchannels_set_fg_rgb(&ret->disablechannels, 0xdddddd);
         nccell c = CELL_TRIVIAL_INITIALIZER;
-        nccell_set_fg_alpha(&c, CELL_ALPHA_TRANSPARENT);
-        nccell_set_bg_alpha(&c, CELL_ALPHA_TRANSPARENT);
+        nccell_set_fg_alpha(&c, NCALPHA_TRANSPARENT);
+        nccell_set_bg_alpha(&c, NCALPHA_TRANSPARENT);
         ncplane_set_base_cell(ret->ncp, &c);
         nccell_release(ret->ncp, &c);
         if(write_header(ret) == 0){
diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c
index 8b93095618..c0ad27cc5a 100644
--- a/src/lib/notcurses.c
+++ b/src/lib/notcurses.c
@@ -943,20 +943,6 @@ void init_lang(struct notcurses* nc){
   }
 }
 
-int ncinputlayer_init(ncinputlayer* nilayer, FILE* infp){
-  setbuffer(infp, NULL, 0);
-  nilayer->inputescapes = NULL;
-  nilayer->ttyinfd = fileno(infp);
-  if(prep_special_keys(nilayer)){
-    return -1;
-  }
-  nilayer->inputbuf_occupied = 0;
-  nilayer->inputbuf_valid_starts = 0;
-  nilayer->inputbuf_write_at = 0;
-  nilayer->input_events = 0;
-  return 0;
-}
-
 // initialize a recursive mutex lock in a way that works on both glibc + musl
 static int
 recursive_lock_init(pthread_mutex_t *lock){
@@ -983,9 +969,6 @@ recursive_lock_init(pthread_mutex_t *lock){
 }
 
 int notcurses_check_pixel_support(notcurses* nc){
-  if(query_term(&nc->tcache, nc->ttyfd)){
-    return -1;
-  }
   if(nc->tcache.bitmap_supported){
     return 1;
   }
@@ -994,8 +977,7 @@ int notcurses_check_pixel_support(notcurses* nc){
 
 // FIXME cut this up into a few distinct pieces, yearrrgh
 notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
-  notcurses_options defaultopts;
-  memset(&defaultopts, 0, sizeof(defaultopts));
+  notcurses_options defaultopts = { };
   if(!opts){
     opts = &defaultopts;
   }
@@ -1092,7 +1074,7 @@ notcurses* notcurses_core_init(const notcurses_options* opts, FILE* outfp){
     goto err;
   }
   ret->suppress_banner = opts->flags & NCOPTION_SUPPRESS_BANNERS;
-  if(set_fd_nonblocking(ret->tcache.input.ttyinfd, 1, &ret->stdio_blocking_save)){
+  if(set_fd_nonblocking(ret->tcache.input.infd, 1, &ret->stdio_blocking_save)){
     goto err;
   }
   if(ncvisual_init(ret->loglevel)){
@@ -1206,7 +1188,7 @@ int notcurses_stop(notcurses* nc){
   int ret = 0;
   if(nc){
     ret |= notcurses_stop_minimal(nc);
-    ret |= set_fd_nonblocking(nc->tcache.input.ttyinfd, nc->stdio_blocking_save, NULL);
+    ret |= set_fd_nonblocking(nc->tcache.input.infd, nc->stdio_blocking_save, NULL);
     if(nc->stdplane){
       notcurses_drop_planes(nc);
       free_plane(nc->stdplane);
@@ -1230,7 +1212,6 @@ int notcurses_stop(notcurses* nc){
     egcpool_dump(&nc->pool);
     free(nc->lastframe);
     free(nc->rstate.mstream);
-    input_free_esctrie(&nc->tcache.input.inputescapes);
     // get any current stats loaded into stash_stats
     notcurses_stats_reset(nc, NULL);
     if(!nc->suppress_banner){
@@ -1311,7 +1292,7 @@ int ncplane_set_fg_palindex(ncplane* n, int idx){
   }
   n->channels |= CELL_FGDEFAULT_MASK;
   n->channels |= CELL_FG_PALETTE;
-  ncchannels_set_fg_alpha(&n->channels, CELL_ALPHA_OPAQUE);
+  ncchannels_set_fg_alpha(&n->channels, NCALPHA_OPAQUE);
   n->stylemask &= 0xffff00ff;
   n->stylemask |= (idx << 8u);
   return 0;
@@ -1323,7 +1304,7 @@ int ncplane_set_bg_palindex(ncplane* n, int idx){
   }
   n->channels |= CELL_BGDEFAULT_MASK;
   n->channels |= CELL_BG_PALETTE;
-  ncchannels_set_bg_alpha(&n->channels, CELL_ALPHA_OPAQUE);
+  ncchannels_set_bg_alpha(&n->channels, NCALPHA_OPAQUE);
   n->stylemask &= 0xffffff00;
   n->stylemask |= idx;
   return 0;
@@ -2615,11 +2596,11 @@ int notcurses_lex_margins(const char* op, notcurses_options* opts){
 }
 
 int notcurses_inputready_fd(notcurses* n){
-  return n->tcache.input.ttyinfd;
+  return n->tcache.input.infd;
 }
 
 int ncdirect_inputready_fd(ncdirect* n){
-  return n->tcache.input.ttyinfd;
+  return n->tcache.input.infd;
 }
 
 // FIXME speed this up, PoC
diff --git a/src/lib/render.c b/src/lib/render.c
index fc391f2384..3be45f68db 100644
--- a/src/lib/render.c
+++ b/src/lib/render.c
@@ -242,17 +242,17 @@ paint(ncplane* p, struct crender* rvec, int dstleny, int dstlenx,
       }
       const nccell* vis = &p->fb[nfbcellidx(p, y, x)];
 
-      if(nccell_fg_alpha(targc) > CELL_ALPHA_OPAQUE){
+      if(nccell_fg_alpha(targc) > NCALPHA_OPAQUE){
         vis = &p->fb[nfbcellidx(p, y, x)];
         if(nccell_fg_default_p(vis)){
           vis = &p->basecell;
         }
         if(nccell_fg_palindex_p(vis)){
-          if(nccell_fg_alpha(targc) == CELL_ALPHA_TRANSPARENT){
+          if(nccell_fg_alpha(targc) == NCALPHA_TRANSPARENT){
             nccell_set_fg_palindex(targc, nccell_fg_palindex(vis));
           }
         }else{
-          if(nccell_fg_alpha(vis) == CELL_ALPHA_HIGHCONTRAST){
+          if(nccell_fg_alpha(vis) == NCALPHA_HIGHCONTRAST){
             crender->s.highcontrast = true;
             crender->s.hcfgblends = crender->s.fgblends;
             crender->hcfg = cell_fchannel(targc);
@@ -262,9 +262,9 @@ paint(ncplane* p, struct crender* rvec, int dstleny, int dstlenx,
           crender->s.fgblends = fgblends;
           // crender->highcontrast can only be true if we just set it, since we're
           // about to set targc opaque based on crender->highcontrast (and this
-          // entire stanza is conditional on targc not being CELL_ALPHA_OPAQUE).
+          // entire stanza is conditional on targc not being NCALPHA_OPAQUE).
           if(crender->s.highcontrast){
-            nccell_set_fg_alpha(targc, CELL_ALPHA_OPAQUE);
+            nccell_set_fg_alpha(targc, NCALPHA_OPAQUE);
           }
         }
       }
@@ -274,7 +274,7 @@ paint(ncplane* p, struct crender* rvec, int dstleny, int dstlenx,
       // If it's transparent, it has no effect. Otherwise, update the
       // background channel and balpha.
       // Evaluate the background first, in case we have HIGHCONTRAST fg text.
-      if(nccell_bg_alpha(targc) > CELL_ALPHA_OPAQUE){
+      if(nccell_bg_alpha(targc) > NCALPHA_OPAQUE){
         vis = &p->fb[nfbcellidx(p, y, x)];
         // to be on the blitter stacking path, we need
         //  1) crender->s.blittedquads to be non-zero (we're below semigraphics)
@@ -285,7 +285,7 @@ paint(ncplane* p, struct crender* rvec, int dstleny, int dstlenx,
             vis = &p->basecell;
           }
           if(nccell_bg_palindex_p(vis)){
-            if(nccell_bg_alpha(targc) == CELL_ALPHA_TRANSPARENT){
+            if(nccell_bg_alpha(targc) == NCALPHA_TRANSPARENT){
               nccell_set_bg_palindex(targc, nccell_bg_palindex(vis));
             }
           }else{
@@ -298,7 +298,7 @@ paint(ncplane* p, struct crender* rvec, int dstleny, int dstlenx,
             vis = &p->basecell;
           }
           if(nccell_fg_palindex_p(vis)){
-            if(nccell_bg_alpha(targc) == CELL_ALPHA_TRANSPARENT){
+            if(nccell_bg_alpha(targc) == NCALPHA_TRANSPARENT){
               nccell_set_bg_palindex(targc, nccell_fg_palindex(vis));
             }
           }else{
@@ -360,13 +360,13 @@ paint(ncplane* p, struct crender* rvec, int dstleny, int dstlenx,
   }
 }
 
-// it's not a pure memset(), because CELL_ALPHA_OPAQUE is the zero value, and
-// we need CELL_ALPHA_TRANSPARENT
+// it's not a pure memset(), because NCALPHA_OPAQUE is the zero value, and
+// we need NCALPHA_TRANSPARENT
 static inline void
 init_rvec(struct crender* rvec, int totalcells){
   struct crender c = {};
-  nccell_set_fg_alpha(&c.c, CELL_ALPHA_TRANSPARENT);
-  nccell_set_bg_alpha(&c.c, CELL_ALPHA_TRANSPARENT);
+  nccell_set_fg_alpha(&c.c, NCALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(&c.c, NCALPHA_TRANSPARENT);
   for(int t = 0 ; t < totalcells ; ++t){
     memcpy(&rvec[t], &c, sizeof(c));
   }
@@ -377,10 +377,10 @@ init_rvec(struct crender* rvec, int totalcells){
 // against the real background.
 static inline void
 lock_in_highcontrast(nccell* targc, struct crender* crender){
-  if(nccell_fg_alpha(targc) == CELL_ALPHA_TRANSPARENT){
+  if(nccell_fg_alpha(targc) == NCALPHA_TRANSPARENT){
     nccell_set_fg_default(targc);
   }
-  if(nccell_bg_alpha(targc) == CELL_ALPHA_TRANSPARENT){
+  if(nccell_bg_alpha(targc) == NCALPHA_TRANSPARENT){
     nccell_set_bg_default(targc);
   }
   if(crender->s.highcontrast){
@@ -443,7 +443,7 @@ postpaint_cell(nccell* lastframe, int dimx, struct crender* crender,
 
 
 // iterate over the rendered frame, adjusting the foreground colors for any
-// cells marked CELL_ALPHA_HIGHCONTRAST, and clearing any cell covered by a
+// cells marked NCALPHA_HIGHCONTRAST, and clearing any cell covered by a
 // wide glyph to its left.
 //
 // FIXME this cannot be performed at render time (we don't yet know the
diff --git a/src/lib/selector.c b/src/lib/selector.c
index b9d11949cf..079d1093de 100644
--- a/src/lib/selector.c
+++ b/src/lib/selector.c
@@ -70,8 +70,8 @@ static int
 ncselector_draw(ncselector* n){
   ncplane_erase(n->ncp);
   nccell transchar = CELL_TRIVIAL_INITIALIZER;
-  nccell_set_fg_alpha(&transchar, CELL_ALPHA_TRANSPARENT);
-  nccell_set_bg_alpha(&transchar, CELL_ALPHA_TRANSPARENT);
+  nccell_set_fg_alpha(&transchar, NCALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(&transchar, NCALPHA_TRANSPARENT);
   // if we have a title, we'll draw a riser. the riser is two rows tall, and
   // exactly four columns longer than the title, and aligned to the right. we
   // draw a rounded box. the body will blow part or all of the bottom away.
@@ -564,8 +564,8 @@ static int
 ncmultiselector_draw(ncmultiselector* n){
   ncplane_erase(n->ncp);
   nccell transchar = CELL_TRIVIAL_INITIALIZER;
-  nccell_set_fg_alpha(&transchar, CELL_ALPHA_TRANSPARENT);
-  nccell_set_bg_alpha(&transchar, CELL_ALPHA_TRANSPARENT);
+  nccell_set_fg_alpha(&transchar, NCALPHA_TRANSPARENT);
+  nccell_set_bg_alpha(&transchar, NCALPHA_TRANSPARENT);
   // if we have a title, we'll draw a riser. the riser is two rows tall, and
   // exactly four columns longer than the title, and aligned to the right. we
   // draw a rounded box. the body will blow part or all of the bottom away.
diff --git a/src/lib/termdesc.c b/src/lib/termdesc.c
index af359dcfdf..9a4380e77b 100644
--- a/src/lib/termdesc.c
+++ b/src/lib/termdesc.c
@@ -6,11 +6,8 @@
 static inline void
 setup_sixel_bitmaps(tinfo* ti){
   ti->bitmap_supported = true;
-  ti->color_registers = 256;  // assumed default [shrug]
   ti->pixel_init = sixel_init;
   ti->pixel_draw = sixel_draw;
-  ti->sixel_maxx = 4096; // whee!
-  ti->sixel_maxy = 4096;
   ti->pixel_remove = NULL;
   ti->pixel_destroy = sixel_destroy;
   ti->pixel_wipe = sixel_wipe;
@@ -93,12 +90,10 @@ apply_term_heuristics(tinfo* ti, const char* termname, int fd){
     ti->bg_collides_default = 0x1000000;
     ti->sextants = true; // work since bugfix in 0.19.3
     ti->quadrants = true;
-    ti->pixel_query_done = true;
     ti->RGBflag = true;
     setup_kitty_bitmaps(ti, fd);
   }else if(strstr(termname, "alacritty")){
     termname = "Alacritty";
-    ti->alacritty_sixel_hack = true;
     ti->quadrants = true;
     // ti->sextants = true; // alacritty https://github.com/alacritty/alacritty/issues/4409 */
     ti->RGBflag = true;
@@ -156,8 +151,8 @@ apply_term_heuristics(tinfo* ti, const char* termname, int fd){
 }
 
 void free_terminfo_cache(tinfo* ti){
-  pthread_mutex_destroy(&ti->pixel_query);
   free(ti->esctable);
+  ncinputlayer_stop(&ti->input);
 }
 
 // tlen -- size of escape table. tused -- used bytes in same.
@@ -201,27 +196,58 @@ init_terminfo_esc(tinfo* ti, const char* name, escape_e idx,
   return 0;
 }
 
+// Device Attributes; replies with (depending on decTerminalID resource):
+//   ⇒  CSI ? 1 ; 2 c  ("VT100 with Advanced Video Option")
+//   ⇒  CSI ? 1 ; 0 c  ("VT101 with No Options")
+//   ⇒  CSI ? 4 ; 6 c  ("VT132 with Advanced Video and Graphics")
+//   ⇒  CSI ? 6 c  ("VT102")
+//   ⇒  CSI ? 7 c  ("VT131")
+//   ⇒  CSI ? 1 2 ; Ps c  ("VT125")
+//   ⇒  CSI ? 6 2 ; Ps c  ("VT220")
+//   ⇒  CSI ? 6 3 ; Ps c  ("VT320")
+//   ⇒  CSI ? 6 4 ; Ps c  ("VT420")
+#define ESC_DA "\e[c"
+
+// we send an XTSMGRAPHICS to set up 256 color registers (the most we can
+// currently take advantage of; we need at least 64 to use sixel at all.
+// maybe that works, maybe it doesn't. then query both color registers
+// and geometry. send XTGETTCAP for terminal name.
+static int
+send_initial_queries(int fd){
+  const char queries[] = "\x1b[=0c\x1b[>c\x1b[>q\x1bP+q544e\x1b\\\x1b[?1;3;256S\x1b[?2;1;0S\x1b[?1;1;0S" ESC_DA;
+  if(blocking_write(fd, queries, strlen(queries))){
+    return -1;
+  }
+  return 0;
+}
+
 // termname is just the TERM environment variable. some details are not
 // exposed via terminfo, and we must make heuristic decisions based on
 // the detected terminal type, yuck :/.
+// the first thing we do is fire off any queries we have (XTSMGRAPHICS, etc.)
+// with a trailing Device Attributes. all known terminals will reply to a
+// Device Attributes, allowing us to get a negative response if our queries
+// aren't supported by the terminal. we fire it off early because we have a
+// full round trip before getting the reply, which is likely to pace init.
 int interrogate_terminfo(tinfo* ti, int fd, const char* termname, unsigned utf8,
                          unsigned noaltscreen, unsigned nocbreak){
   memset(ti, 0, sizeof(*ti));
-  ti->utf8 = utf8;
   if(fd >= 0){
+    if(send_initial_queries(fd)){
+      fprintf(stderr, "Error issuing terminal queries on %d\n", fd);
+      return -1;
+    }
     if(tcgetattr(fd, &ti->tpreserved)){
       fprintf(stderr, "Couldn't preserve terminal state for %d (%s)\n", fd, strerror(errno));
       return -1;
     }
-  }
-  if(ncinputlayer_init(&ti->input, stdin)){
-    return -1;
-  }
-  if(!nocbreak){
+    // enter cbreak mode regardless of user preference until we've performed
+    // terminal interrogation. at that point, we might restore original mode.
     if(cbreak_mode(fd, &ti->tpreserved)){
       return -1;
     }
   }
+  ti->utf8 = utf8;
   // allow the "rgb" boolean terminfo capability, a COLORTERM environment
   // variable of either "truecolor" or "24bit", or unconditionally enable it
   // for several terminals known to always support 8bpc rgb setaf/setab.
@@ -371,272 +397,29 @@ int interrogate_terminfo(tinfo* ti, int fd, const char* termname, unsigned utf8,
       goto err;
     }
   }
-  pthread_mutex_init(&ti->pixel_query, NULL);
-  ti->pixel_query_done = false;
-  if(apply_term_heuristics(ti, termname, fd)){
-    pthread_mutex_destroy(&ti->pixel_query);
+  if(ncinputlayer_init(ti, stdin)){
     goto err;
   }
-  return 0;
-
-err:
-  free(ti->esctable);
-  return -1;
-}
-
-// FIXME need unit tests on this
-// FIXME can read a character not intended for it
-// we'll get a trailing Device Attributes response, because we write
-// XTSMGRAPHICS followed by a DA query, in case the former isn't supported
-// (we'd otherwise hang looking for input).
-static int
-read_xtsmgraphics_reply(int fd, int* val2){
-  char in;
-  // return is of the form CSI ? Pi ; Ps ; Pv S
-  // Pi: 1 color registers, 2 sixel, 3 regis
-  // Pa: 1 read, 2 reset, 3 set to Pv, 4 read maximum
-  // Pv: n for color registers, width;height for geometry
-  // Ps: 0 success, 1 bad Pi, 2 bad Pa, 3 failure
-  enum {
-    WANT_CSI,
-    WANT_QMARK,
-    WANT_PI,
-    WANT_SEMI1,
-    WANT_SEMI2,
-    WANT_PV1,
-    WANT_PV2,
-    DONE,
-  } state = WANT_CSI;
-  int pv = 0;
-  int pi = 0;
-  while(read(fd, &in, 1) == 1){
-//fprintf(stderr, "READ: %c 0x%02x\n", in, in);
-    if(in == 'c'){ // should match the end of DA
-      break;
-    }
-    switch(state){
-      case WANT_CSI:
-        if(in == NCKEY_ESC){
-          state = WANT_QMARK;
-        }
-        break;
-      case WANT_QMARK:
-        if(in == '?'){
-          state = WANT_PI;
-        }
-        break;
-      case WANT_PI:
-        if(!isdigit(in)){
-          break;
-        }
-        pi *= 10;
-        pi += in - '0';
-        if(pi >= 1 && pi <= 3){
-          state = WANT_SEMI1;
-        }
-        break;
-      case WANT_SEMI1:
-        if(in == ';'){
-          state = WANT_SEMI2;
-        }
-        break;
-      case WANT_SEMI2:
-        if(in == ';'){
-          state = WANT_PV1;
-        }
-        break;
-      case WANT_PV1:
-        if(in == 'S'){
-          state = DONE;
-        }else if(val2 && in == ';'){
-          *val2 = 0;
-          state = WANT_PV2;
-        }else if(isdigit(in)){
-          pv *= 10;
-          pv += in - '0';
-        }
-        break;
-      case WANT_PV2:
-        if(in == 'S'){
-          state = DONE;
-        }else if(isdigit(in)){
-          *val2 *= 10;
-          *val2 += in - '0';
-        }
-        break;
-      case DONE:
-      default:
-        break;
-    }
+  // our current sixel quantization algorithm requires at least 64 color
+  // registers. we make use of no more than 256.
+  if(ti->color_registers >= 64){
+    setup_sixel_bitmaps(ti);
   }
-  if(state == DONE){
-    if(pv >= 0 && (!val2 || *val2 >= 0)){
-      return pv;
-    }
-  }
-  return -1;
-}
-
-static int
-query_xtsmgraphics(int fd, const char* seq, int* val, int* val2){
-  if(blocking_write(fd, seq, strlen(seq))){
-    return -1;
-  }
-  int r = read_xtsmgraphics_reply(fd, val2);
-  if(r <= 0){
-    return -1;
-  }
-  *val = r;
-  return 0;
-}
-
-// Device Attributes; replies with (depending on decTerminalID resource):
-//   ⇒  CSI ? 1 ; 2 c  ("VT100 with Advanced Video Option")
-//   ⇒  CSI ? 1 ; 0 c  ("VT101 with No Options")
-//   ⇒  CSI ? 4 ; 6 c  ("VT132 with Advanced Video and Graphics")
-//   ⇒  CSI ? 6 c  ("VT102")
-//   ⇒  CSI ? 7 c  ("VT131")
-//   ⇒  CSI ? 1 2 ; Ps c  ("VT125")
-//   ⇒  CSI ? 6 2 ; Ps c  ("VT220")
-//   ⇒  CSI ? 6 3 ; Ps c  ("VT320")
-//   ⇒  CSI ? 6 4 ; Ps c  ("VT420")
-#define ESC_DA "\e[c"
-
-// query for Sixel details including the number of color registers and, one day
-// perhaps, maximum geometry. xterm binds its return by the current geometry,
-// making it useless for a one-time query.
-static int
-query_sixel_details(tinfo* ti, int fd){
-  if(query_xtsmgraphics(fd, "\x1b[?2;1;0S" ESC_DA, &ti->sixel_maxx, &ti->sixel_maxy)){
-    return -1;
-  }
-  if(query_xtsmgraphics(fd, "\x1b[?1;1;0S" ESC_DA, &ti->color_registers, NULL)){
-    return -1;
-  }
-//fprintf(stderr, "Sixel ColorRegs: %d Max_x: %d Max_y: %d\n", ti->color_registers, ti->sixel_maxx, ti->sixel_maxy);
-  if(ti->color_registers < 64){ // FIXME try to drive it higher
-    return -1;
-  }
-  return 0;
-}
-
-// query for Sixel support
-static int
-query_sixel(tinfo* ti, int fd){
-  if(blocking_write(fd, ESC_DA, strlen(ESC_DA))){
-    return -1;
-  }
-  char in;
-  // perhaps the most lackadaisical response is that of st, which returns a
-  // bare ESC[?6c (note no semicolon). this is equivalent to alacritty's
-  // return, both suggesting a VT102. alacritty's miraculous technicolor VT102
-  // can display sixel, but real VT102s don't even reply to XTSMGRAPHICS, so we
-  // detect VT102 + TERM including alacritty, and special-case that.
-  // FIXME need unit tests on this
-  enum {
-    WANT_CSI,
-    WANT_QMARK,
-    WANT_SEMI,
-    WANT_C4, // want '4' to indicate sixel or 'c' to terminate
-    WANT_VT102_C,
-    DONE
-  } state = WANT_CSI;
-  bool in4 = false; // set true after seeing ";4", clear on semi
-  // we're looking for a 4 following a semicolon
-  while(state != DONE && read(fd, &in, 1) == 1){
-    switch(state){
-      case WANT_CSI:
-        if(in == NCKEY_ESC){
-          state = WANT_QMARK;
-        }
-        break;
-      case WANT_QMARK:
-        if(in == '?'){
-          state = WANT_SEMI;
-        }
-        break;
-      case WANT_SEMI:
-        if(in == ';'){
-          if(in4){
-            setup_sixel_bitmaps(ti);
-          }
-          state = WANT_C4;
-        }else if(in == 'c'){
-          if(in4){
-            setup_sixel_bitmaps(ti);
-          }
-          state = DONE;
-        }else if(in == '6'){
-          state = WANT_VT102_C;
-        }
-        in4 = false;
-        break;
-      case WANT_VT102_C:
-        if(in == 'c'){
-          if(ti->alacritty_sixel_hack){
-            setup_sixel_bitmaps(ti);
-          }
-          state = DONE;
-        }else if(in == ';'){
-          state = WANT_C4;
-        }
-        break;
-      case WANT_C4:
-        if(in == 'c'){
-          state = DONE;
-        }else if(in == '4'){
-          in4 = true;
-          state = WANT_SEMI;
-        }else{
-          in4 = false;
-        }
-        break;
-      case DONE:
-      default:
-        break;
+  if(nocbreak){
+    if(fd >= 0){
+      if(tcsetattr(fd, TCSANOW, &ti->tpreserved)){
+        ncinputlayer_stop(&ti->input);
+        goto err;
+      }
     }
   }
-  if(ti->bitmap_supported){
-    if(query_sixel_details(ti, fd)){
-      ti->bitmap_supported = false;
-    }
+  if(apply_term_heuristics(ti, termname, fd)){
+    ncinputlayer_stop(&ti->input);
+    goto err;
   }
   return 0;
-}
 
-// fd must be a real terminal. uses the query lock of |ti| to only act once.
-// we ought already have performed a TIOCGWINSZ ioctl() to verify that the
-// terminal reports cell area in pixels, as that's necessary for our use of
-// sixel (or any other bitmap protocol).
-int query_term(tinfo* ti, int fd){
-  if(fd < 0){
-    return -1;
-  }
-  int flags = fcntl(fd, F_GETFL, 0);
-  if(flags < 0){
-    return -1;
-  }
-  int ret = 0;
-  pthread_mutex_lock(&ti->pixel_query);
-  if(!ti->pixel_query_done){
-    // if the terminal reported 0 pixels for cell dimensions, bypass any
-    // interrogation, and assume no bitmap support.
-    if(!ti->cellpixx || !ti->cellpixy){
-      ti->pixel_query_done = true;
-    }else{
-      if(flags & O_NONBLOCK){
-        fcntl(fd, F_SETFL, flags & ~O_NONBLOCK);
-      }
-      ret = query_sixel(ti, fd);
-      ti->pixel_query_done = true;
-      if(ti->bitmap_supported){
-        ti->pixel_init(fd);
-      }
-      if(flags & O_NONBLOCK){
-        fcntl(fd, F_SETFL, flags);
-      }
-    }
-  }
-  pthread_mutex_unlock(&ti->pixel_query);
-  return ret;
+err:
+  free(ti->esctable);
+  return -1;
 }
diff --git a/src/lib/termdesc.h b/src/lib/termdesc.h
index 0de0f54714..d5017646b9 100644
--- a/src/lib/termdesc.h
+++ b/src/lib/termdesc.h
@@ -59,8 +59,17 @@ typedef enum {
   ESCAPE_MAX
 } escape_e;
 
+// we read input from one or two places. if stdin is connected to our
+// controlling tty, we read only from that file descriptor. if it is
+// connected to something else, and we have a controlling tty, we will
+// read data only from stdin and control only from the tty. if we have
+// no connected tty, only data is available.
 typedef struct ncinputlayer {
-  int ttyinfd;  // file descriptor for processing input
+  // ttyfd is only valid if we are connected to a tty, *and* stdin is not
+  // connected to that tty. in that case, we read control sequences only
+  // from ttyfd.
+  int ttyfd; // file descriptor for connected tty
+  int infd;  // file descriptor for processing input, from stdin
   unsigned char inputbuf[BUFSIZ];
   // we keep a wee ringbuffer of input queued up for delivery. if
   // inputbuf_occupied == sizeof(inputbuf), there is no room. otherwise, data
@@ -102,7 +111,6 @@ typedef struct tinfo {
   // which we support sixel and kitty. the kitty protocol is used based
   // on TERM heuristics. otherwise, we attempt to detect sixel support, and
   // query the details of the implementation.
-  pthread_mutex_t pixel_query; // only query for pixel support once
   int color_registers; // sixel color registers (post pixel_query_done)
   int sixel_maxx, sixel_maxy; // sixel size maxima (post pixel_query_done)
   int (*pixel_destroy)(const struct notcurses* nc, const struct ncpile* p, FILE* out, struct sprixel* s);
@@ -123,7 +131,6 @@ typedef struct tinfo {
   ncinputlayer input;       // input layer
   bool bitmap_supported;    // do we support bitmaps (post pixel_query_done)?
   bool bitmap_lowest_line;  // can we render pixels to the bottom row?
-  bool pixel_query_done;    // have we yet performed pixel query?
   bool RGBflag;   // "RGB" flag for 24bpc truecolor
   bool CCCflag;   // "CCC" flag for palette set capability
   bool BCEflag;   // "BCE" flag for erases with background color
@@ -137,13 +144,6 @@ typedef struct tinfo {
   bool sextants;  // do we have (good, vetted) Unicode 13 sextant support?
   bool braille;   // do we have Braille support? (linux console does not)
 
-  // alacritty went rather off the reservation for their sixel support. they
-  // reply to DSA with CSI?6c, meaning VT102, but no VT102 had Sixel support,
-  // so if the TERM variable contains "alacritty", *and* we get VT102, we go
-  // ahead and query XTSMGRAPHICS.
-  // FIXME we can get rid of this with proper input handling #1469
-  bool alacritty_sixel_hack;
-
   // mlterm resets the cursor (i.e. makes it visible) any time you print
   // a sprixel. we work around this spiritedly unorthodox decision.
   bool sprixel_cursor_hack; // do sprixels reset the cursor? (mlterm)
diff --git a/src/ls/main.cpp b/src/ls/main.cpp
index 9c457e140a..ca8d3a5e97 100644
--- a/src/ls/main.cpp
+++ b/src/ls/main.cpp
@@ -266,7 +266,6 @@ int main(int argc, char* const * argv){
     blitter,
     scale,
   };
-  ctx.nc.check_pixel_support();
   keep_working = true;
   for(auto s = 0u ; s < procs ; ++s){
     threads.emplace_back(std::thread(ncls_thread, &ctx));
diff --git a/src/player/play.cpp b/src/player/play.cpp
index 023f97a8e8..8db882d8d2 100644
--- a/src/player/play.cpp
+++ b/src/player/play.cpp
@@ -62,12 +62,12 @@ auto handle_subtitle(char* subtitle, struct marshal* marsh,
     };
     marsh->subtitle_plane = ncplane_create(vopts->n, &nopts);
     uint64_t channels = 0;
-    ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
-    ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+    ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
+    ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
     ncplane_set_base(marsh->subtitle_plane, "", 0, channels);
     ncplane_set_fg_rgb(marsh->subtitle_plane, 0x00ffff);
-    ncplane_set_fg_alpha(marsh->subtitle_plane, CELL_ALPHA_HIGHCONTRAST);
-    ncplane_set_bg_alpha(marsh->subtitle_plane, CELL_ALPHA_TRANSPARENT);
+    ncplane_set_fg_alpha(marsh->subtitle_plane, NCALPHA_HIGHCONTRAST);
+    ncplane_set_bg_alpha(marsh->subtitle_plane, NCALPHA_TRANSPARENT);
   }else{
     ncplane_erase(marsh->subtitle_plane);
   }
@@ -378,8 +378,8 @@ int rendered_mode_player_inner(NotCurses& nc, int argc, char** argv,
   int dimy, dimx;
   std::unique_ptr<Plane> stdn(nc.get_stdplane(&dimy, &dimx));
   uint64_t transchan = 0;
-  ncchannels_set_fg_alpha(&transchan, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_bg_alpha(&transchan, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&transchan, NCALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&transchan, NCALPHA_TRANSPARENT);
   stdn->set_base("", 0, transchan);
   struct ncplane_options nopts{};
   nopts.name = "play";
diff --git a/src/poc/multiselect.c b/src/poc/multiselect.c
index 2c82f88b99..13862e9c4c 100644
--- a/src/poc/multiselect.c
+++ b/src/poc/multiselect.c
@@ -86,8 +86,8 @@ int main(void){
   sopts.footchannels = CHANNELS_RGB_INITIALIZER(0xe0, 0, 0x40, 0x20, 0x20, 0);
   sopts.titlechannels = CHANNELS_RGB_INITIALIZER(0x20, 0xff, 0xff, 0, 0, 0x20);
   uint64_t bgchannels = CHANNELS_RGB_INITIALIZER(0, 0x20, 0, 0, 0x20, 0);
-  ncchannels_set_fg_alpha(&bgchannels, CELL_ALPHA_BLEND);
-  ncchannels_set_bg_alpha(&bgchannels, CELL_ALPHA_BLEND);
+  ncchannels_set_fg_alpha(&bgchannels, NCALPHA_BLEND);
+  ncchannels_set_bg_alpha(&bgchannels, NCALPHA_BLEND);
   struct ncplane* n = notcurses_stdplane(nc);
 
   if(notcurses_canopen_images(nc)){
diff --git a/src/poc/selector.c b/src/poc/selector.c
index 647d83677b..bfc5fd16b7 100644
--- a/src/poc/selector.c
+++ b/src/poc/selector.c
@@ -77,8 +77,8 @@ int main(void){
   sopts.footchannels = CHANNELS_RGB_INITIALIZER(0xe0, 0, 0x40, 0x20, 0, 0);
   sopts.titlechannels = CHANNELS_RGB_INITIALIZER(0xff, 0xff, 0x80, 0, 0, 0x20);
   uint64_t bgchannels = CHANNELS_RGB_INITIALIZER(0, 0x20, 0, 0, 0x20, 0);
-  ncchannels_set_fg_alpha(&bgchannels, CELL_ALPHA_BLEND);
-  ncchannels_set_bg_alpha(&bgchannels, CELL_ALPHA_BLEND);
+  ncchannels_set_fg_alpha(&bgchannels, NCALPHA_BLEND);
+  ncchannels_set_bg_alpha(&bgchannels, NCALPHA_BLEND);
   struct ncplane* n = notcurses_stdplane(nc);
 
   if(notcurses_canopen_images(nc)){
diff --git a/src/poc/textplay.c b/src/poc/textplay.c
index 1132cc5338..cad6a1333c 100644
--- a/src/poc/textplay.c
+++ b/src/poc/textplay.c
@@ -192,8 +192,8 @@ textplane(struct notcurses* nc){
   };
   struct ncplane* n = ncplane_create(stdn, &nopts);
   uint64_t channels = CHANNELS_RGB_INITIALIZER(0, 0, 0, 0x22, 0x22, 0x22);
-  /*ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);*/
+  /*ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);*/
   if(n){
     ncplane_set_base(n, "", 0, channels);
   }
diff --git a/src/poc/wipebitmap.c b/src/poc/wipebitmap.c
index 53f1c1f7a2..b80d531d45 100644
--- a/src/poc/wipebitmap.c
+++ b/src/poc/wipebitmap.c
@@ -44,8 +44,8 @@ wipebitmap(struct notcurses* nc){
     }
   }
   uint64_t channels = 0;
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
   ncplane_set_base(notcurses_stdplane(nc), "", 0, channels);
   ncplane_move_top(notcurses_stdplane(nc));
   emit(notcurses_stdplane(nc), "Ought see 16 *s");
diff --git a/src/tetris/background.h b/src/tetris/background.h
index c8e753d02a..298bb5929c 100644
--- a/src/tetris/background.h
+++ b/src/tetris/background.h
@@ -19,16 +19,16 @@ void DrawBoard() { // draw all fixed components of the game
                                           board_top_y_, x / 2 - (BOARD_WIDTH + 1));
   uint64_t channels = 0;
   ncchannels_set_fg_rgb(&channels, 0x00b040);
-  ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
   board_->double_box(0, channels, BOARD_HEIGHT - 1, BOARD_WIDTH * 2 - 1, NCBOXMASK_TOP);
-  ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
   board_->set_base("", 0, channels);
   scoreplane_ = std::make_unique<ncpp::Plane>(2, 30, y - BOARD_HEIGHT, 2, nullptr);
   uint64_t scorechan = 0;
-  ncchannels_set_bg_alpha(&scorechan, CELL_ALPHA_TRANSPARENT);
-  ncchannels_set_fg_alpha(&scorechan, CELL_ALPHA_TRANSPARENT);
+  ncchannels_set_bg_alpha(&scorechan, NCALPHA_TRANSPARENT);
+  ncchannels_set_fg_alpha(&scorechan, NCALPHA_TRANSPARENT);
   scoreplane_->set_base("", 0, scorechan);
-  scoreplane_->set_bg_alpha(CELL_ALPHA_TRANSPARENT);
+  scoreplane_->set_bg_alpha(NCALPHA_TRANSPARENT);
   scoreplane_->set_fg_rgb(0xd040d0);
   scoreplane_->printf(0, 1, "%s", getpwuid(geteuid())->pw_name);
   scoreplane_->set_fg_rgb(0x00d0a0);
diff --git a/src/tetris/newpiece.h b/src/tetris/newpiece.h
index fb1a891e8d..d60686fd82 100644
--- a/src/tetris/newpiece.h
+++ b/src/tetris/newpiece.h
@@ -19,10 +19,10 @@ std::unique_ptr<ncpp::Plane> NewPiece() {
   std::unique_ptr<ncpp::Plane> n = std::make_unique<ncpp::Plane>(2, cols, board_top_y_ - 1, xoff, nullptr);
   if(n){
     uint64_t channels = 0;
-    ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
-    ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+    ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
+    ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
     n->set_fg_rgb(t->color);
-    n->set_bg_alpha(CELL_ALPHA_TRANSPARENT);
+    n->set_bg_alpha(NCALPHA_TRANSPARENT);
     n->set_base("", 0, channels);
     y = 0; x = 0;
     for(size_t i = 0 ; i < strlen(t->texture) ; ++i){


Test patch:
diff --git a/rust/src/cells/test/reimplemented.rs b/rust/src/cells/test/reimplemented.rs
index b03da33ea8..e9c0480bcc 100644
--- a/rust/src/cells/test/reimplemented.rs
+++ b/rust/src/cells/test/reimplemented.rs
@@ -41,11 +41,11 @@ fn alpha() {
     assert_eq![0, crate::nccell_fg_alpha(&c1)];
     assert_eq![0, crate::nccell_bg_alpha(&c1)];
 
-    crate::nccell_set_fg_alpha(&mut c1, crate::NCCELL_ALPHA_TRANSPARENT);
-    assert_eq![crate::NCCELL_ALPHA_TRANSPARENT, crate::nccell_fg_alpha(&c1)];
+    crate::nccell_set_fg_alpha(&mut c1, crate::NCCELL_TRANSPARENT);
+    assert_eq![crate::NCCELL_TRANSPARENT, crate::nccell_fg_alpha(&c1)];
 
-    crate::nccell_set_bg_alpha(&mut c1, crate::NCCELL_ALPHA_BLEND);
-    assert_eq![crate::NCCELL_ALPHA_BLEND, crate::nccell_bg_alpha(&c1)];
+    crate::nccell_set_bg_alpha(&mut c1, crate::NCCELL_BLEND);
+    assert_eq![crate::NCCELL_BLEND, crate::nccell_bg_alpha(&c1)];
 }
 
 #[test]
diff --git a/rust/src/channel/test/reimplemented.rs b/rust/src/channel/test/reimplemented.rs
index 10a58cb891..ad1e8b47cb 100644
--- a/rust/src/channel/test/reimplemented.rs
+++ b/rust/src/channel/test/reimplemented.rs
@@ -3,8 +3,8 @@
 use serial_test::serial;
 
 use crate::{
-    NcChannel, NcChannelPair, NCCELL_ALPHA_BLEND, NCCELL_ALPHA_HIGHCONTRAST, NCCELL_ALPHA_OPAQUE,
-    NCCELL_ALPHA_TRANSPARENT,
+    NcChannel, NcChannelPair, NCCELL_BLEND, NCCELL_HIGHCONTRAST, NCCELL_OPAQUE,
+    NCCELL_TRANSPARENT,
 };
 
 // NcChannel tests -------------------------------------------------------------
@@ -89,10 +89,10 @@ fn channel_set() {
 #[serial]
 fn channel_alpha() {
     let c: NcChannel = 0x112233;
-    assert_ne!(crate::ncchannel_alpha(c), NCCELL_ALPHA_TRANSPARENT);
+    assert_ne!(crate::ncchannel_alpha(c), NCCELL_TRANSPARENT);
 
-    let c: NcChannel = 0x112233 | NCCELL_ALPHA_TRANSPARENT;
-    assert_eq!(crate::ncchannel_alpha(c), NCCELL_ALPHA_TRANSPARENT);
+    let c: NcChannel = 0x112233 | NCCELL_TRANSPARENT;
+    assert_eq!(crate::ncchannel_alpha(c), NCCELL_TRANSPARENT);
 }
 
 /// sets the alpha component
@@ -100,17 +100,17 @@ fn channel_alpha() {
 #[serial]
 fn channel_set_alpha() {
     let mut c: NcChannel = 0x112233;
-    crate::ncchannel_set_alpha(&mut c, NCCELL_ALPHA_HIGHCONTRAST);
-    assert_eq!(NCCELL_ALPHA_HIGHCONTRAST, crate::ncchannel_alpha(c));
+    crate::ncchannel_set_alpha(&mut c, NCCELL_HIGHCONTRAST);
+    assert_eq!(NCCELL_HIGHCONTRAST, crate::ncchannel_alpha(c));
 
-    crate::ncchannel_set_alpha(&mut c, NCCELL_ALPHA_TRANSPARENT);
-    assert_eq!(NCCELL_ALPHA_TRANSPARENT, crate::ncchannel_alpha(c));
+    crate::ncchannel_set_alpha(&mut c, NCCELL_TRANSPARENT);
+    assert_eq!(NCCELL_TRANSPARENT, crate::ncchannel_alpha(c));
 
-    crate::ncchannel_set_alpha(&mut c, NCCELL_ALPHA_BLEND);
-    assert_eq!(NCCELL_ALPHA_BLEND, crate::ncchannel_alpha(c));
+    crate::ncchannel_set_alpha(&mut c, NCCELL_BLEND);
+    assert_eq!(NCCELL_BLEND, crate::ncchannel_alpha(c));
 
-    crate::ncchannel_set_alpha(&mut c, NCCELL_ALPHA_OPAQUE);
-    assert_eq!(NCCELL_ALPHA_OPAQUE, crate::ncchannel_alpha(c));
+    crate::ncchannel_set_alpha(&mut c, NCCELL_OPAQUE);
+    assert_eq!(NCCELL_OPAQUE, crate::ncchannel_alpha(c));
     // TODO: CHECK for NCCELL_BGDEFAULT_MASK
 }
 
@@ -123,7 +123,7 @@ fn channel_set_default() {
     assert_eq!(true, crate::ncchannel_default_p(channel));
 
     // If we change it from being opaque...
-    let mut channel_transp = channel | NCCELL_ALPHA_TRANSPARENT;
+    let mut channel_transp = channel | NCCELL_TRANSPARENT;
     assert_eq!(0x_20_112233, channel_transp); // the transparent bit is now set
 
     crate::ncchannel_set_not_default(&mut channel_transp);
@@ -162,7 +162,7 @@ fn channel_default_p() {
     let mut c: NcChannel = 0x112233;
     assert_eq!(true, crate::ncchannel_default_p(c));
 
-    let _ = crate::ncchannel_set_alpha(&mut c, NCCELL_ALPHA_OPAQUE);
+    let _ = crate::ncchannel_set_alpha(&mut c, NCCELL_OPAQUE);
     assert_eq!(true, crate::ncchannel_default_p(c));
 
     crate::ncchannel_set(&mut c, 0x112233);
diff --git a/src/tests/bitmap.cpp b/src/tests/bitmap.cpp
index 4f61e7b986..95f78c16c6 100644
--- a/src/tests/bitmap.cpp
+++ b/src/tests/bitmap.cpp
@@ -636,8 +636,8 @@ TEST_CASE("Bitmaps") {
 #ifdef NOTCURSES_USE_MULTIMEDIA
   SUBCASE("PixelWipeImage") {
     uint64_t channels = 0;
-    ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
-    ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+    ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT);
+    ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
     CHECK(0 == ncplane_set_base(n_, "", 0, channels));
     auto ncv = ncvisual_from_file(find_data("worldmap.png").get());
     REQUIRE(ncv);
diff --git a/src/tests/cell.cpp b/src/tests/cell.cpp
index 351505e132..a4a579e696 100644
--- a/src/tests/cell.cpp
+++ b/src/tests/cell.cpp
@@ -125,12 +125,12 @@ TEST_CASE("Cell") {
     nccell c = CELL_TRIVIAL_INITIALIZER;
     CHECK(0 > nccell_set_fg_alpha(&c, -1));
     CHECK(0 > nccell_set_fg_alpha(&c, 4));
-    CHECK(0 == nccell_set_fg_alpha(&c, CELL_ALPHA_OPAQUE));
+    CHECK(0 == nccell_set_fg_alpha(&c, NCALPHA_OPAQUE));
     CHECK(nccell_fg_default_p(&c));
     CHECK(nccell_bg_default_p(&c));
-    CHECK(CELL_ALPHA_OPAQUE == nccell_fg_alpha(&c));
-    CHECK(0 == nccell_set_fg_alpha(&c, CELL_ALPHA_HIGHCONTRAST));
-    CHECK(CELL_ALPHA_HIGHCONTRAST == nccell_fg_alpha(&c));
+    CHECK(NCALPHA_OPAQUE == nccell_fg_alpha(&c));
+    CHECK(0 == nccell_set_fg_alpha(&c, NCALPHA_HIGHCONTRAST));
+    CHECK(NCALPHA_HIGHCONTRAST == nccell_fg_alpha(&c));
     CHECK(!nccell_fg_default_p(&c));
     CHECK(nccell_bg_default_p(&c));
   }
@@ -139,11 +139,11 @@ TEST_CASE("Cell") {
     nccell c = CELL_TRIVIAL_INITIALIZER;
     CHECK(0 > nccell_set_bg_alpha(&c, -1));
     CHECK(0 > nccell_set_bg_alpha(&c, 4));
-    CHECK(0 == nccell_set_bg_alpha(&c, CELL_ALPHA_OPAQUE));
-    CHECK(CELL_ALPHA_OPAQUE == nccell_bg_alpha(&c));
-    CHECK(0 != nccell_set_bg_alpha(&c, CELL_ALPHA_HIGHCONTRAST));
-    CHECK(0 == nccell_set_bg_alpha(&c, CELL_ALPHA_TRANSPARENT));
-    CHECK(CELL_ALPHA_TRANSPARENT == nccell_bg_alpha(&c));
+    CHECK(0 == nccell_set_bg_alpha(&c, NCALPHA_OPAQUE));
+    CHECK(NCALPHA_OPAQUE == nccell_bg_alpha(&c));
+    CHECK(0 != nccell_set_bg_alpha(&c, NCALPHA_HIGHCONTRAST));
+    CHECK(0 == nccell_set_bg_alpha(&c, NCALPHA_TRANSPARENT));
+    CHECK(NCALPHA_TRANSPARENT == nccell_bg_alpha(&c));
     CHECK(nccell_fg_default_p(&c));
     CHECK(!nccell_bg_default_p(&c));
   }
@@ -152,8 +152,8 @@ TEST_CASE("Cell") {
   SUBCASE("HighContrastWhiteOnBlackBackground"){
     nccell c = CELL_CHAR_INITIALIZER('+');
     CHECK(0 == nccell_set_fg_rgb8(&c, 0xff, 0xff, 0xff));
-    CHECK(0 == nccell_set_fg_alpha(&c, CELL_ALPHA_HIGHCONTRAST));
-    CHECK(0 == nccell_set_bg_alpha(&c, CELL_ALPHA_TRANSPARENT));
+    CHECK(0 == nccell_set_fg_alpha(&c, NCALPHA_HIGHCONTRAST));
+    CHECK(0 == nccell_set_bg_alpha(&c, NCALPHA_TRANSPARENT));
     struct ncplane_options nopts = {
       .y = 0,
       .x = 0,
@@ -170,7 +170,7 @@ TEST_CASE("Cell") {
     CHECK(1 == ncplane_putc(np, &c));
     nccell_load_char(np, &c, '*');
     CHECK(0 == nccell_set_bg_rgb8(&c, 0x0, 0x0, 0x0));
-    CHECK(0 == nccell_set_bg_alpha(&c, CELL_ALPHA_OPAQUE));
+    CHECK(0 == nccell_set_bg_alpha(&c, NCALPHA_OPAQUE));
     CHECK(1 == ncplane_putc(n_, &c));
     CHECK(0 == notcurses_render(nc_));
     uint64_t channels, underchannels, overchannels;
@@ -192,8 +192,8 @@ TEST_CASE("Cell") {
   SUBCASE("HighContrastWhiteOnWhiteBackground"){
     nccell c = CELL_CHAR_INITIALIZER('+');
     CHECK(0 == nccell_set_fg_rgb8(&c, 0xff, 0xff, 0xff));
-    CHECK(0 == nccell_set_fg_alpha(&c, CELL_ALPHA_HIGHCONTRAST));
-    CHECK(0 == nccell_set_bg_alpha(&c, CELL_ALPHA_TRANSPARENT));
+    CHECK(0 == nccell_set_fg_alpha(&c, NCALPHA_HIGHCONTRAST));
+    CHECK(0 == nccell_set_bg_alpha(&c, NCALPHA_TRANSPARENT));
     struct ncplane_options nopts = {
       .y = 0,
       .x = 0,
@@ -210,7 +210,7 @@ TEST_CASE("Cell") {
     CHECK(1 == ncplane_putc(np, &c));
     nccell_load_char(np, &c, '*');
     CHECK(0 == nccell_set_bg_rgb8(&c, 0xff, 0xff, 0xff));
-    CHECK(0 == nccell_set_bg_alpha(&c, CELL_ALPHA_OPAQUE));
+    CHECK(0 == nccell_set_bg_alpha(&c, NCALPHA_OPAQUE));
     CHECK(1 == ncplane_putc(n_, &c));
     CHECK(0 == notcurses_render(nc_));
     uint64_t channels, underchannels, overchannels;
@@ -232,8 +232,8 @@ TEST_CASE("Cell") {
   SUBCASE("HighContrastBlackOnBlackBackground"){
     nccell c = CELL_CHAR_INITIALIZER('+');
     CHECK(0 == nccell_set_fg_rgb8(&c, 0x0, 0x0, 0x0));
-    CHECK(0 == nccell_set_fg_alpha(&c, CELL_ALPHA_HIGHCONTRAST));
-    CHECK(0 == nccell_set_bg_alpha(&c, CELL_ALPHA_TRANSPARENT));
+    CHECK(0 == nccell_set_fg_alpha(&c, NCALPHA_HIGHCONTRAST));
+    CHECK(0 == nccell_set_bg_alpha(&c, NCALPHA_TRANSPARENT));
     struct ncplane_options nopts = {
       .y = 0,
       .x = 0,
@@ -250,7 +250,7 @@ TEST_CASE("Cell") {
     CHECK(1 == ncplane_putc(np, &c));
     nccell_load_char(np, &c, '*');
     CHECK(0 == nccell_set_bg_rgb8(&c, 0x0, 0x0, 0x0));
-    CHECK(0 == nccell_set_bg_alpha(&c, CELL_ALPHA_OPAQUE));
+    CHECK(0 == nccell_set_bg_alpha(&c, NCALPHA_OPAQUE));
     CHECK(1 == ncplane_putc(n_, &c));
     CHECK(0 == notcurses_render(nc_));
     uint64_t channels, underchannels, overchannels;
@@ -272,8 +272,8 @@ TEST_CASE("Cell") {
   SUBCASE("HighContrastBlackOnWhiteBackground"){
     nccell c = CELL_CHAR_INITIALIZER('+');
     CHECK(0 == nccell_set_fg_rgb8(&c, 0x0, 0x0, 0x0));
-    CHECK(0 == nccell_set_fg_alpha(&c, CELL_ALPHA_HIGHCONTRAST));
-    CHECK(0 == nccell_set_bg_alpha(&c, CELL_ALPHA_TRANSPARENT));
+    CHECK(0 == nccell_set_fg_alpha(&c, NCALPHA_HIGHCONTRAST));
+    CHECK(0 == nccell_set_bg_alpha(&c, NCALPHA_TRANSPARENT));
     struct ncplane_options nopts = {
       .y = 0,
       .x = 0,
@@ -290,7 +290,7 @@ TEST_CASE("Cell") {
     CHECK(1 == ncplane_putc(np, &c));
     nccell_load_char(np, &c, '*');
     CHECK(0 == nccell_set_bg_rgb8(&c, 0xff, 0xff, 0xff));
-    CHECK(0 == nccell_set_bg_alpha(&c, CELL_ALPHA_OPAQUE));
+    CHECK(0 == nccell_set_bg_alpha(&c, NCALPHA_OPAQUE));
     CHECK(1 == ncplane_putc(n_, &c));
     CHECK(0 == notcurses_render(nc_));
     uint64_t channels, underchannels, overchannels;
@@ -314,7 +314,7 @@ TEST_CASE("Cell") {
     nccell c = CELL_TRIVIAL_INITIALIZER;
     // top has a background of white
     CHECK(0 == nccell_set_bg_rgb8(&c, 0xff, 0xff, 0xff));
-    CHECK(0 == nccell_set_fg_alpha(&c, CELL_ALPHA_TRANSPARENT));
+    CHECK(0 == nccell_set_fg_alpha(&c, NCALPHA_TRANSPARENT));
     struct ncplane_options nopts = {
       .y = 0,
       .x = 0,
@@ -332,7 +332,7 @@ TEST_CASE("Cell") {
     nccell_load_char(n_, &c, '*');
     // bottom has white foreground + HIGHCONTRAST, should remain white
     CHECK(0 == nccell_set_fg_rgb8(&c, 0xff, 0x0, 0xff));
-    CHECK(0 == nccell_set_fg_alpha(&c, CELL_ALPHA_HIGHCONTRAST));
+    CHECK(0 == nccell_set_fg_alpha(&c, NCALPHA_HIGHCONTRAST));
     nccell_set_bg_default(&c);
     CHECK(1 == ncplane_putc(n_, &c));
     CHECK(0 == notcurses_render(nc_));
diff --git a/src/tests/channel.cpp b/src/tests/channel.cpp
index f950d40017..a44c4f87c8 100644
--- a/src/tests/channel.cpp
+++ b/src/tests/channel.cpp
@@ -27,12 +27,12 @@ TEST_CASE("ChannelGetAlpha") {
     uint32_t channel;
     int a;
   } test[] = {
-    { .channel = 0x00000000, .a = CELL_ALPHA_OPAQUE, },
-    { .channel = 0x10808080, .a = CELL_ALPHA_BLEND, },
-    { .channel = 0x20080808, .a = CELL_ALPHA_TRANSPARENT, },
-    { .channel = 0xe0080808, .a = CELL_ALPHA_TRANSPARENT, },
-    { .channel = 0x3fffffff, .a = CELL_ALPHA_HIGHCONTRAST, },
-    { .channel = 0xffffffff, .a = CELL_ALPHA_HIGHCONTRAST, },
+    { .channel = 0x00000000, .a = NCALPHA_OPAQUE, },
+    { .channel = 0x10808080, .a = NCALPHA_BLEND, },
+    { .channel = 0x20080808, .a = NCALPHA_TRANSPARENT, },
+    { .channel = 0xe0080808, .a = NCALPHA_TRANSPARENT, },
+    { .channel = 0x3fffffff, .a = NCALPHA_HIGHCONTRAST, },
+    { .channel = 0xffffffff, .a = NCALPHA_HIGHCONTRAST, },
   };
   for(auto i = 0u ; i < sizeof(test) / sizeof(*test) ; ++i){
     CHECK(test[i].a == ncchannel_alpha(test[i].channel));
diff --git a/src/tests/notcurses.cpp b/src/tests/notcurses.cpp
index 33d04bc716..07a642ec5c 100644
--- a/src/tests/notcurses.cpp
+++ b/src/tests/notcurses.cpp
@@ -105,12 +105,12 @@ TEST_CASE("NotcursesBase") {
     uint64_t channel = 0;
     CHECK(0 > ncchannels_set_fg_alpha(&channel, -1));
     CHECK(0 > ncchannels_set_fg_alpha(&channel, 4));
-    CHECK(0 == ncchannels_set_fg_alpha(&channel, CELL_ALPHA_OPAQUE));
-    CHECK(CELL_ALPHA_OPAQUE == ncchannels_fg_alpha(channel));
+    CHECK(0 == ncchannels_set_fg_alpha(&channel, NCALPHA_OPAQUE));
+    CHECK(NCALPHA_OPAQUE == ncchannels_fg_alpha(channel));
     CHECK(ncchannels_fg_default_p(channel));
     CHECK(ncchannels_bg_default_p(channel));
-    CHECK(0 == ncchannels_set_fg_alpha(&channel, CELL_ALPHA_HIGHCONTRAST));
-    CHECK(CELL_ALPHA_HIGHCONTRAST == ncchannels_fg_alpha(channel));
+    CHECK(0 == ncchannels_set_fg_alpha(&channel, NCALPHA_HIGHCONTRAST));
+    CHECK(NCALPHA_HIGHCONTRAST == ncchannels_fg_alpha(channel));
     CHECK(!ncchannels_fg_default_p(channel));
     CHECK(ncchannels_bg_default_p(channel));
   }
@@ -119,11 +119,11 @@ TEST_CASE("NotcursesBase") {
     uint64_t channel = 0;
     CHECK(0 > ncchannels_set_bg_alpha(&channel, -1));
     CHECK(0 > ncchannels_set_bg_alpha(&channel, 4));
-    CHECK(0 == ncchannels_set_bg_alpha(&channel, CELL_ALPHA_OPAQUE));
-    CHECK(CELL_ALPHA_OPAQUE == ncchannels_bg_alpha(channel));
-    CHECK(0 == ncchannels_set_bg_alpha(&channel, CELL_ALPHA_TRANSPARENT));
-    CHECK(0 > ncchannels_set_bg_alpha(&channel, CELL_ALPHA_HIGHCONTRAST));
-    CHECK(CELL_ALPHA_TRANSPARENT == ncchannels_bg_alpha(channel));
+    CHECK(0 == ncchannels_set_bg_alpha(&channel, NCALPHA_OPAQUE));
+    CHECK(NCALPHA_OPAQUE == ncchannels_bg_alpha(channel));
+    CHECK(0 == ncchannels_set_bg_alpha(&channel, NCALPHA_TRANSPARENT));
+    CHECK(0 > ncchannels_set_bg_alpha(&channel, NCALPHA_HIGHCONTRAST));
+    CHECK(NCALPHA_TRANSPARENT == ncchannels_bg_alpha(channel));
     CHECK(ncchannels_fg_default_p(channel));
     CHECK(!ncchannels_bg_default_p(channel));
   }
diff --git a/src/tests/palette.cpp b/src/tests/palette.cpp
index f11abd879a..9b673f0665 100644
--- a/src/tests/palette.cpp
+++ b/src/tests/palette.cpp
@@ -42,22 +42,22 @@ TEST_CASE("Palette256") {
   SUBCASE("FAttributes") {
     nccell c = CELL_TRIVIAL_INITIALIZER;
     CHECK(nccell_fg_default_p(&c));
-    nccell_set_fg_alpha(&c, CELL_ALPHA_TRANSPARENT);
+    nccell_set_fg_alpha(&c, NCALPHA_TRANSPARENT);
     CHECK(0 == nccell_set_fg_palindex(&c, 0x20));
     CHECK(!nccell_fg_default_p(&c));
     CHECK(nccell_fg_palindex_p(&c));
-    CHECK(CELL_ALPHA_OPAQUE == nccell_fg_alpha(&c));
+    CHECK(NCALPHA_OPAQUE == nccell_fg_alpha(&c));
     CHECK(0x20 == nccell_fg_palindex(&c));
   }
 
   SUBCASE("BAttributes") {
     nccell c = CELL_TRIVIAL_INITIALIZER;
     CHECK(nccell_bg_default_p(&c));
-    nccell_set_bg_alpha(&c, CELL_ALPHA_TRANSPARENT);
+    nccell_set_bg_alpha(&c, NCALPHA_TRANSPARENT);
     CHECK(0 == nccell_set_bg_palindex(&c, 0x20));
     CHECK(!nccell_bg_default_p(&c));
     CHECK(nccell_bg_palindex_p(&c));
-    CHECK(CELL_ALPHA_OPAQUE == nccell_bg_alpha(&c));
+    CHECK(NCALPHA_OPAQUE == nccell_bg_alpha(&c));
     CHECK(0x20 == nccell_bg_palindex(&c));
   }
 
@@ -73,8 +73,8 @@ TEST_CASE("Palette256") {
     CHECK(0 < ncplane_at_yx_cell(n_, 0, 0, &r));
     CHECK(nccell_fg_palindex_p(&r));
     CHECK(nccell_bg_palindex_p(&r));
-    CHECK(CELL_ALPHA_OPAQUE == nccell_fg_alpha(&r));
-    CHECK(CELL_ALPHA_OPAQUE == nccell_bg_alpha(&r));
+    CHECK(NCALPHA_OPAQUE == nccell_fg_alpha(&r));
+    CHECK(NCALPHA_OPAQUE == nccell_bg_alpha(&r));
     CHECK(0x20 == nccell_fg_palindex(&r));
     CHECK(0x40 == nccell_bg_palindex(&r));
     nccell_release(n_, &r);
@@ -94,8 +94,8 @@ TEST_CASE("Palette256") {
     CHECK(nullptr != notcurses_at_yx(nc_, 0, 0, &r.stylemask, &r.channels));
     CHECK(nccell_fg_palindex_p(&r));
     CHECK(nccell_bg_palindex_p(&r));
-    CHECK(CELL_ALPHA_OPAQUE == nccell_fg_alpha(&r));
-    CHECK(CELL_ALPHA_OPAQUE == nccell_bg_alpha(&r));
+    CHECK(NCALPHA_OPAQUE == nccell_fg_alpha(&r));
+    CHECK(NCALPHA_OPAQUE == nccell_bg_alpha(&r));
     CHECK(0x20 == nccell_fg_palindex(&r));
     CHECK(0x40 == nccell_bg_palindex(&r));
     nccell_release(n_, &r);
diff --git a/src/tests/plot.cpp b/src/tests/plot.cpp
index 8d0c1890a9..6e95966f77 100644
--- a/src/tests/plot.cpp
+++ b/src/tests/plot.cpp
@@ -158,8 +158,8 @@ TEST_CASE("Plot") {
     memset(&popts, 0, sizeof(popts));
     popts.maxchannels = CHANNELS_RGB_INITIALIZER(0xff, 0xff, 0xff, 0, 0, 0);
     popts.minchannels = CHANNELS_RGB_INITIALIZER(0, 0xff, 0, 0, 0, 0);
-    ncchannels_set_bg_alpha(&popts.minchannels, CELL_ALPHA_BLEND);
-    ncchannels_set_fg_alpha(&popts.minchannels, CELL_ALPHA_BLEND);
+    ncchannels_set_bg_alpha(&popts.minchannels, NCALPHA_BLEND);
+    ncchannels_set_fg_alpha(&popts.minchannels, NCALPHA_BLEND);
     popts.gridtype = NCBLIT_BRAILLE;
     auto p = ncuplot_create(ncp, &popts, 0, 0);
     REQUIRE(p);
@@ -189,8 +189,8 @@ TEST_CASE("Plot") {
     memset(&popts, 0, sizeof(popts));
     popts.maxchannels = CHANNELS_RGB_INITIALIZER(0xff, 0xff, 0xff, 0, 0, 0);
     popts.minchannels = CHANNELS_RGB_INITIALIZER(0, 0xff, 0, 0, 0, 0);
-    ncchannels_set_bg_alpha(&popts.minchannels, CELL_ALPHA_BLEND);
-    ncchannels_set_fg_alpha(&popts.minchannels, CELL_ALPHA_BLEND);
+    ncchannels_set_bg_alpha(&popts.minchannels, NCALPHA_BLEND);
+    ncchannels_set_fg_alpha(&popts.minchannels, NCALPHA_BLEND);
     popts.gridtype = NCBLIT_2x2;
     auto p = ncuplot_create(ncp, &popts, 0, 0);
     REQUIRE(p);
@@ -216,8 +216,8 @@ TEST_CASE("Plot") {
     memset(&popts, 0, sizeof(popts));
     popts.maxchannels = CHANNELS_RGB_INITIALIZER(0xff, 0xff, 0xff, 0, 0, 0);
     popts.minchannels = CHANNELS_RGB_INITIALIZER(0, 0xff, 0, 0, 0, 0);
-    ncchannels_set_bg_alpha(&popts.minchannels, CELL_ALPHA_BLEND);
-    ncchannels_set_fg_alpha(&popts.minchannels, CELL_ALPHA_BLEND);
+    ncchannels_set_bg_alpha(&popts.minchannels, NCALPHA_BLEND);
+    ncchannels_set_fg_alpha(&popts.minchannels, NCALPHA_BLEND);
     popts.gridtype = NCBLIT_8x1;
     auto p = ncuplot_create(ncp, &popts, 0, 0);
     REQUIRE(p);
@@ -240,8 +240,8 @@ TEST_CASE("Plot") {
     memset(&popts, 0, sizeof(popts));
     popts.maxchannels = CHANNELS_RGB_INITIALIZER(0xff, 0xff, 0xff, 0, 0, 0);
     popts.minchannels = CHANNELS_RGB_INITIALIZER(0, 0xff, 0, 0, 0, 0);
-    ncchannels_set_bg_alpha(&popts.minchannels, CELL_ALPHA_BLEND);
-    ncchannels_set_fg_alpha(&popts.minchannels, CELL_ALPHA_BLEND);
+    ncchannels_set_bg_alpha(&popts.minchannels, NCALPHA_BLEND);
+    ncchannels_set_fg_alpha(&popts.minchannels, NCALPHA_BLEND);
     popts.gridtype = NCBLIT_3x2;
     auto p = ncuplot_create(ncp, &popts, 0, 0);
     REQUIRE(p);
@@ -267,8 +267,8 @@ TEST_CASE("Plot") {
     memset(&popts, 0, sizeof(popts));
     popts.maxchannels = CHANNELS_RGB_INITIALIZER(0xff, 0xff, 0xff, 0, 0, 0);
     popts.minchannels = CHANNELS_RGB_INITIALIZER(0, 0xff, 0, 0, 0, 0);
-    ncchannels_set_bg_alpha(&popts.minchannels, CELL_ALPHA_BLEND);
-    ncchannels_set_fg_alpha(&popts.minchannels, CELL_ALPHA_BLEND);
+    ncchannels_set_bg_alpha(&popts.minchannels, NCALPHA_BLEND);
+    ncchannels_set_fg_alpha(&popts.minchannels, NCALPHA_BLEND);
     popts.gridtype = NCBLIT_BRAILLE;
     auto p = ncuplot_create(ncp, &popts, 0, 0);
     REQUIRE(p);
diff --git a/src/tests/reel.cpp b/src/tests/reel.cpp
index 45414be84e..f8ef44d251 100644
--- a/src/tests/reel.cpp
+++ b/src/tests/reel.cpp
@@ -289,7 +289,7 @@ TEST_CASE("Reels") {
     int dimy, dimx;
     notcurses_term_dim_yx(nc_, &dimy, &dimx);
     uint64_t channels = 0;
-    ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT);
+    ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT);
     struct ncplane_options nopts = {
       .y = 0,
       .x = 0,
diff --git a/src/tests/rotate.cpp b/src/tests/rotate.cpp
index 045df32c49..27d32ba3b1 100644
--- a/src/tests/rotate.cpp
+++ b/src/tests/rotate.cpp
@@ -64,8 +64,8 @@ TEST_CASE("Rotate") {
     };
     struct ncplane* testn = ncplane_create(n_, &nopts);
     uint64_t channels = 0;
-    CHECK(0 == ncchannels_set_fg_alpha(&channels, CELL_ALPHA_TRANSPARENT));
-    CHECK(0 == ncchannels_set_bg_alpha(&channels, CELL_ALPHA_TRANSPARENT));
+    CHECK(0 == ncchannels_set_fg_alpha(&channels, NCALPHA_TRANSPARENT));
+    CHECK(0 == ncchannels_set_bg_alpha(&channels, NCALPHA_TRANSPARENT));
     REQUIRE(0 >= ncplane_set_base(testn, "", 0, channels));
     nccell tl = CELL_TRIVIAL_INITIALIZER, tr = CELL_TRIVIAL_INITIALIZER;
     nccell bl = CELL_TRIVIAL_INITIALIZER, br = CELL_TRIVIAL_INITIALIZER;
diff --git a/src/tests/wide.cpp b/src/tests/wide.cpp
index 1cb40d4ab5..343d821027 100644
--- a/src/tests/wide.cpp
+++ b/src/tests/wide.cpp
@@ -416,7 +416,7 @@ TEST_CASE("Wide") {
     int sbytes;
     CHECK(2 == ncplane_putegc_yx(n_, 1, 1, "六", &sbytes));
     uint64_t channels = 0;
-    ncchannels_set_bg_alpha(&channels, CELL_ALPHA_BLEND);
+    ncchannels_set_bg_alpha(&channels, NCALPHA_BLEND);
     ncchannels_set_bg_rgb8(&channels, 0x80, 0xf0, 0x10);
     CHECK(1 == ncplane_set_base(p, " ", 0, channels));
     CHECK(0 == notcurses_render(nc_));