Repository: dankamongmen/notcurses (language: python)

Issue:
add missing capabilities functions to direct mode
Right now it's not possible to check the same capabilities in direct mode as in full mode. The missing ones in direct mode are:

- [x] ncdirect_canbraille()
- [x] ncdirect_cansextant()
- [x] ncdirect_canquadrant()
- [x] ncdirect_canhalfblock()
- [x] ncdirect_canopen_images()
- [x] ncdirect_canopen_videos()
- [x] ncdirect_cantruecolor()
- [x] ncdirect_canfade()
- [x] ncdirect_canchangecolor()

add missing capabilities functions to direct mode
Right now it's not possible to check the same capabilities in direct mode as in full mode. The missing ones in direct mode are:

- [x] ncdirect_canbraille()
- [x] ncdirect_cansextant()
- [x] ncdirect_canquadrant()
- [x] ncdirect_canhalfblock()
- [x] ncdirect_canopen_images()
- [x] ncdirect_canopen_videos()
- [x] ncdirect_cantruecolor()
- [x] ncdirect_canfade()
- [x] ncdirect_canchangecolor()

add missing capabilities functions to direct mode
Right now it's not possible to check the same capabilities in direct mode as in full mode. The missing ones in direct mode are:

- [x] ncdirect_canbraille()
- [x] ncdirect_cansextant()
- [x] ncdirect_canquadrant()
- [x] ncdirect_canhalfblock()
- [x] ncdirect_canopen_images()
- [x] ncdirect_canopen_videos()
- [x] ncdirect_cantruecolor()
- [x] ncdirect_canfade()
- [x] ncdirect_canchangecolor()

add missing capabilities functions to direct mode
Right now it's not possible to check the same capabilities in direct mode as in full mode. The missing ones in direct mode are:

- [x] ncdirect_canbraille()
- [x] ncdirect_cansextant()
- [x] ncdirect_canquadrant()
- [x] ncdirect_canhalfblock()
- [x] ncdirect_canopen_images()
- [x] ncdirect_canopen_videos()
- [x] ncdirect_cantruecolor()
- [x] ncdirect_canfade()
- [x] ncdirect_canchangecolor()

add missing capabilities functions to direct mode
Right now it's not possible to check the same capabilities in direct mode as in full mode. The missing ones in direct mode are:

- [x] ncdirect_canbraille()
- [x] ncdirect_cansextant()
- [x] ncdirect_canquadrant()
- [x] ncdirect_canhalfblock()
- [x] ncdirect_canopen_images()
- [x] ncdirect_canopen_videos()
- [x] ncdirect_cantruecolor()
- [x] ncdirect_canfade()
- [x] ncdirect_canchangecolor()



Discussion / hints:
i hesitated to add all of these because i didn't like the idea of symbol explosion. maybe i'll add a `ncdirect_get_tinfo()` primarily for internal use (but it would need be exposed), and then implement all of these as inline functions. we'd then express each of these as a `tinfo_can*()` function (these would also need be exported, yuck), and reimplement the `notcurses_can*()` functions atop them. we couldn't make these latter `inline` just yet (ABI breakage), but we could for ABI3.

or maybe i just shouldn't care about the number of exported symbols.
Part of my motivation for asking this is that I'm currently wrapping all the capabilities in a single [`Capabilities`](https://github.com/dankamongmen/notcurses-rs/blob/master/src/capabilities.rs) struct (in `notcurses-rs`), and the idea was to be able to get the same struct either from Notcurses or NcDirect.

> or maybe i just shouldn't care about the number of exported symbols.

I surrendered long ago to the humongous number of functions of the C API, lol... My goal in the higher-level rust bindings is to provide a minimal and versatile public API that hides most of that.
> Part of my motivation for asking this is that I'm currently wrapping all the capabilities in a single [`Capabilities`](https://github.com/dankamongmen/notcurses-rs/blob/master/src/capabilities.rs) struct (in `notcurses-rs`), and the idea was to be able to get the same struct either from Notcurses or NcDirect.

that isn't a bad idea at all. i think i might introduce exactly that for ncdirect. thanks for the suggestion!

> I surrendered long ago to the humongous number of functions of the C API, lol... 

```
[schwarzgerat](0) $ wc -l src/dsscaw/notcurses/debian/libnotcurses-core2.symbols 
386 src/dsscaw/notcurses/debian/libnotcurses-core2.symbols
[schwarzgerat](0) $ 
```

damn! i had no idea it had grown that large.
> that isn't a bad idea at all. i think i might introduce exactly that for ncdirect. thanks for the suggestion!

If you do that, then go all the way and change how's done in the notcurses context too. After all, it's usually a single time operation, and having all of them updated together in a single place is handy.

And you can remove a lot of functions in a single shot XD
> If you do that, then go all the way and change how's done in the notcurses context too. After all, it's usually a single time operation, and having all of them updated together in a single place is handy.

of course, but i can't do anything like that until ABI3. removing a symbol is an ABI break, even when replaced with an inline (already-compiled programs will fail to link). i would mark them deprecated, and then kill them off for 3.0.0.
so i suggest the following three functions to replace all existing capability functions:

```c
typedef struct {
 // blah blah blah
} nccapabilites;
struct nccapabilities* notcurses_capabilities(const struct notcurses*);
struct nccapabilities* ncdirect_capabilities(const struct ncdirect*);
void nccapabilities_destroy(struct nccapabilities*);
```

we need to originate the object inside the library boundary, or else we can't freely add new fields in the future (since older clients would pass too small an object). we could have a `nccapabilities_create(void)`, but why bother? we don't need reuse this object, so there's no reason to accept a preexisting one. so just always heap-allocate and return, *but make the definition public* so we don't need to have two dozen functions accessing fields.

c++ can wrap this with an RAII using `nccapabilites_destroy()` as a custom deallocator, and i assume you can do something with rust to hide the dealloc. c users eat shit, as usual.
hrmmmmmmmmmmmmmmmmmmmm.

no sir, i don't like it. let's instead do:

```c
typedef struct {
} nccapabilites;
const struct nccapabilites* notcurses_...
const struct nccapabilites* ncdirect_...
```

and we will have a single `nccapabilites` struct inside the `notcurses` and `ncdirect` objects. we'll hand back a const reference. user is responsible for not using past the lifetime of the parent object. eliminates allocation and population cost, plus need to call destructor, at the cost of making possible unsafe late references. well, don't do that.
i've started this work in `dankamongmen/direct-capabilities`
`ncdirect_canopen_image()` already exists, btw
i hesitated to add all of these because i didn't like the idea of symbol explosion. maybe i'll add a `ncdirect_get_tinfo()` primarily for internal use (but it would need be exposed), and then implement all of these as inline functions. we'd then express each of these as a `tinfo_can*()` function (these would also need be exported, yuck), and reimplement the `notcurses_can*()` functions atop them. we couldn't make these latter `inline` just yet (ABI breakage), but we could for ABI3.

or maybe i just shouldn't care about the number of exported symbols.
Part of my motivation for asking this is that I'm currently wrapping all the capabilities in a single [`Capabilities`](https://github.com/dankamongmen/notcurses-rs/blob/master/src/capabilities.rs) struct (in `notcurses-rs`), and the idea was to be able to get the same struct either from Notcurses or NcDirect.

> or maybe i just shouldn't care about the number of exported symbols.

I surrendered long ago to the humongous number of functions of the C API, lol... My goal in the higher-level rust bindings is to provide a minimal and versatile public API that hides most of that.
> Part of my motivation for asking this is that I'm currently wrapping all the capabilities in a single [`Capabilities`](https://github.com/dankamongmen/notcurses-rs/blob/master/src/capabilities.rs) struct (in `notcurses-rs`), and the idea was to be able to get the same struct either from Notcurses or NcDirect.

that isn't a bad idea at all. i think i might introduce exactly that for ncdirect. thanks for the suggestion!

> I surrendered long ago to the humongous number of functions of the C API, lol... 

```
[schwarzgerat](0) $ wc -l src/dsscaw/notcurses/debian/libnotcurses-core2.symbols 
386 src/dsscaw/notcurses/debian/libnotcurses-core2.symbols
[schwarzgerat](0) $ 
```

damn! i had no idea it had grown that large.
> that isn't a bad idea at all. i think i might introduce exactly that for ncdirect. thanks for the suggestion!

If you do that, then go all the way and change how's done in the notcurses context too. After all, it's usually a single time operation, and having all of them updated together in a single place is handy.

And you can remove a lot of functions in a single shot XD
> If you do that, then go all the way and change how's done in the notcurses context too. After all, it's usually a single time operation, and having all of them updated together in a single place is handy.

of course, but i can't do anything like that until ABI3. removing a symbol is an ABI break, even when replaced with an inline (already-compiled programs will fail to link). i would mark them deprecated, and then kill them off for 3.0.0.
so i suggest the following three functions to replace all existing capability functions:

```c
typedef struct {
 // blah blah blah
} nccapabilites;
struct nccapabilities* notcurses_capabilities(const struct notcurses*);
struct nccapabilities* ncdirect_capabilities(const struct ncdirect*);
void nccapabilities_destroy(struct nccapabilities*);
```

we need to originate the object inside the library boundary, or else we can't freely add new fields in the future (since older clients would pass too small an object). we could have a `nccapabilities_create(void)`, but why bother? we don't need reuse this object, so there's no reason to accept a preexisting one. so just always heap-allocate and return, *but make the definition public* so we don't need to have two dozen functions accessing fields.

c++ can wrap this with an RAII using `nccapabilites_destroy()` as a custom deallocator, and i assume you can do something with rust to hide the dealloc. c users eat shit, as usual.
hrmmmmmmmmmmmmmmmmmmmm.

no sir, i don't like it. let's instead do:

```c
typedef struct {
} nccapabilites;
const struct nccapabilites* notcurses_...
const struct nccapabilites* ncdirect_...
```

and we will have a single `nccapabilites` struct inside the `notcurses` and `ncdirect` objects. we'll hand back a const reference. user is responsible for not using past the lifetime of the parent object. eliminates allocation and population cost, plus need to call destructor, at the cost of making possible unsafe late references. well, don't do that.
i've started this work in `dankamongmen/direct-capabilities`
`ncdirect_canopen_image()` already exists, btw
i hesitated to add all of these because i didn't like the idea of symbol explosion. maybe i'll add a `ncdirect_get_tinfo()` primarily for internal use (but it would need be exposed), and then implement all of these as inline functions. we'd then express each of these as a `tinfo_can*()` function (these would also need be exported, yuck), and reimplement the `notcurses_can*()` functions atop them. we couldn't make these latter `inline` just yet (ABI breakage), but we could for ABI3.

or maybe i just shouldn't care about the number of exported symbols.
Part of my motivation for asking this is that I'm currently wrapping all the capabilities in a single [`Capabilities`](https://github.com/dankamongmen/notcurses-rs/blob/master/src/capabilities.rs) struct (in `notcurses-rs`), and the idea was to be able to get the same struct either from Notcurses or NcDirect.

> or maybe i just shouldn't care about the number of exported symbols.

I surrendered long ago to the humongous number of functions of the C API, lol... My goal in the higher-level rust bindings is to provide a minimal and versatile public API that hides most of that.
> Part of my motivation for asking this is that I'm currently wrapping all the capabilities in a single [`Capabilities`](https://github.com/dankamongmen/notcurses-rs/blob/master/src/capabilities.rs) struct (in `notcurses-rs`), and the idea was to be able to get the same struct either from Notcurses or NcDirect.

that isn't a bad idea at all. i think i might introduce exactly that for ncdirect. thanks for the suggestion!

> I surrendered long ago to the humongous number of functions of the C API, lol... 

```
[schwarzgerat](0) $ wc -l src/dsscaw/notcurses/debian/libnotcurses-core2.symbols 
386 src/dsscaw/notcurses/debian/libnotcurses-core2.symbols
[schwarzgerat](0) $ 
```

damn! i had no idea it had grown that large.
> that isn't a bad idea at all. i think i might introduce exactly that for ncdirect. thanks for the suggestion!

If you do that, then go all the way and change how's done in the notcurses context too. After all, it's usually a single time operation, and having all of them updated together in a single place is handy.

And you can remove a lot of functions in a single shot XD
> If you do that, then go all the way and change how's done in the notcurses context too. After all, it's usually a single time operation, and having all of them updated together in a single place is handy.

of course, but i can't do anything like that until ABI3. removing a symbol is an ABI break, even when replaced with an inline (already-compiled programs will fail to link). i would mark them deprecated, and then kill them off for 3.0.0.
so i suggest the following three functions to replace all existing capability functions:

```c
typedef struct {
 // blah blah blah
} nccapabilites;
struct nccapabilities* notcurses_capabilities(const struct notcurses*);
struct nccapabilities* ncdirect_capabilities(const struct ncdirect*);
void nccapabilities_destroy(struct nccapabilities*);
```

we need to originate the object inside the library boundary, or else we can't freely add new fields in the future (since older clients would pass too small an object). we could have a `nccapabilities_create(void)`, but why bother? we don't need reuse this object, so there's no reason to accept a preexisting one. so just always heap-allocate and return, *but make the definition public* so we don't need to have two dozen functions accessing fields.

c++ can wrap this with an RAII using `nccapabilites_destroy()` as a custom deallocator, and i assume you can do something with rust to hide the dealloc. c users eat shit, as usual.
hrmmmmmmmmmmmmmmmmmmmm.

no sir, i don't like it. let's instead do:

```c
typedef struct {
} nccapabilites;
const struct nccapabilites* notcurses_...
const struct nccapabilites* ncdirect_...
```

and we will have a single `nccapabilites` struct inside the `notcurses` and `ncdirect` objects. we'll hand back a const reference. user is responsible for not using past the lifetime of the parent object. eliminates allocation and population cost, plus need to call destructor, at the cost of making possible unsafe late references. well, don't do that.
i've started this work in `dankamongmen/direct-capabilities`
`ncdirect_canopen_image()` already exists, btw
i hesitated to add all of these because i didn't like the idea of symbol explosion. maybe i'll add a `ncdirect_get_tinfo()` primarily for internal use (but it would need be exposed), and then implement all of these as inline functions. we'd then express each of these as a `tinfo_can*()` function (these would also need be exported, yuck), and reimplement the `notcurses_can*()` functions atop them. we couldn't make these latter `inline` just yet (ABI breakage), but we could for ABI3.

or maybe i just shouldn't care about the number of exported symbols.
Part of my motivation for asking this is that I'm currently wrapping all the capabilities in a single [`Capabilities`](https://github.com/dankamongmen/notcurses-rs/blob/master/src/capabilities.rs) struct (in `notcurses-rs`), and the idea was to be able to get the same struct either from Notcurses or NcDirect.

> or maybe i just shouldn't care about the number of exported symbols.

I surrendered long ago to the humongous number of functions of the C API, lol... My goal in the higher-level rust bindings is to provide a minimal and versatile public API that hides most of that.
> Part of my motivation for asking this is that I'm currently wrapping all the capabilities in a single [`Capabilities`](https://github.com/dankamongmen/notcurses-rs/blob/master/src/capabilities.rs) struct (in `notcurses-rs`), and the idea was to be able to get the same struct either from Notcurses or NcDirect.

that isn't a bad idea at all. i think i might introduce exactly that for ncdirect. thanks for the suggestion!

> I surrendered long ago to the humongous number of functions of the C API, lol... 

```
[schwarzgerat](0) $ wc -l src/dsscaw/notcurses/debian/libnotcurses-core2.symbols 
386 src/dsscaw/notcurses/debian/libnotcurses-core2.symbols
[schwarzgerat](0) $ 
```

damn! i had no idea it had grown that large.
> that isn't a bad idea at all. i think i might introduce exactly that for ncdirect. thanks for the suggestion!

If you do that, then go all the way and change how's done in the notcurses context too. After all, it's usually a single time operation, and having all of them updated together in a single place is handy.

And you can remove a lot of functions in a single shot XD
> If you do that, then go all the way and change how's done in the notcurses context too. After all, it's usually a single time operation, and having all of them updated together in a single place is handy.

of course, but i can't do anything like that until ABI3. removing a symbol is an ABI break, even when replaced with an inline (already-compiled programs will fail to link). i would mark them deprecated, and then kill them off for 3.0.0.
so i suggest the following three functions to replace all existing capability functions:

```c
typedef struct {
 // blah blah blah
} nccapabilites;
struct nccapabilities* notcurses_capabilities(const struct notcurses*);
struct nccapabilities* ncdirect_capabilities(const struct ncdirect*);
void nccapabilities_destroy(struct nccapabilities*);
```

we need to originate the object inside the library boundary, or else we can't freely add new fields in the future (since older clients would pass too small an object). we could have a `nccapabilities_create(void)`, but why bother? we don't need reuse this object, so there's no reason to accept a preexisting one. so just always heap-allocate and return, *but make the definition public* so we don't need to have two dozen functions accessing fields.

c++ can wrap this with an RAII using `nccapabilites_destroy()` as a custom deallocator, and i assume you can do something with rust to hide the dealloc. c users eat shit, as usual.
hrmmmmmmmmmmmmmmmmmmmm.

no sir, i don't like it. let's instead do:

```c
typedef struct {
} nccapabilites;
const struct nccapabilites* notcurses_...
const struct nccapabilites* ncdirect_...
```

and we will have a single `nccapabilites` struct inside the `notcurses` and `ncdirect` objects. we'll hand back a const reference. user is responsible for not using past the lifetime of the parent object. eliminates allocation and population cost, plus need to call destructor, at the cost of making possible unsafe late references. well, don't do that.
i've started this work in `dankamongmen/direct-capabilities`
`ncdirect_canopen_image()` already exists, btw
i hesitated to add all of these because i didn't like the idea of symbol explosion. maybe i'll add a `ncdirect_get_tinfo()` primarily for internal use (but it would need be exposed), and then implement all of these as inline functions. we'd then express each of these as a `tinfo_can*()` function (these would also need be exported, yuck), and reimplement the `notcurses_can*()` functions atop them. we couldn't make these latter `inline` just yet (ABI breakage), but we could for ABI3.

or maybe i just shouldn't care about the number of exported symbols.
Part of my motivation for asking this is that I'm currently wrapping all the capabilities in a single [`Capabilities`](https://github.com/dankamongmen/notcurses-rs/blob/master/src/capabilities.rs) struct (in `notcurses-rs`), and the idea was to be able to get the same struct either from Notcurses or NcDirect.

> or maybe i just shouldn't care about the number of exported symbols.

I surrendered long ago to the humongous number of functions of the C API, lol... My goal in the higher-level rust bindings is to provide a minimal and versatile public API that hides most of that.
> Part of my motivation for asking this is that I'm currently wrapping all the capabilities in a single [`Capabilities`](https://github.com/dankamongmen/notcurses-rs/blob/master/src/capabilities.rs) struct (in `notcurses-rs`), and the idea was to be able to get the same struct either from Notcurses or NcDirect.

that isn't a bad idea at all. i think i might introduce exactly that for ncdirect. thanks for the suggestion!

> I surrendered long ago to the humongous number of functions of the C API, lol... 

```
[schwarzgerat](0) $ wc -l src/dsscaw/notcurses/debian/libnotcurses-core2.symbols 
386 src/dsscaw/notcurses/debian/libnotcurses-core2.symbols
[schwarzgerat](0) $ 
```

damn! i had no idea it had grown that large.
> that isn't a bad idea at all. i think i might introduce exactly that for ncdirect. thanks for the suggestion!

If you do that, then go all the way and change how's done in the notcurses context too. After all, it's usually a single time operation, and having all of them updated together in a single place is handy.

And you can remove a lot of functions in a single shot XD
> If you do that, then go all the way and change how's done in the notcurses context too. After all, it's usually a single time operation, and having all of them updated together in a single place is handy.

of course, but i can't do anything like that until ABI3. removing a symbol is an ABI break, even when replaced with an inline (already-compiled programs will fail to link). i would mark them deprecated, and then kill them off for 3.0.0.
so i suggest the following three functions to replace all existing capability functions:

```c
typedef struct {
 // blah blah blah
} nccapabilites;
struct nccapabilities* notcurses_capabilities(const struct notcurses*);
struct nccapabilities* ncdirect_capabilities(const struct ncdirect*);
void nccapabilities_destroy(struct nccapabilities*);
```

we need to originate the object inside the library boundary, or else we can't freely add new fields in the future (since older clients would pass too small an object). we could have a `nccapabilities_create(void)`, but why bother? we don't need reuse this object, so there's no reason to accept a preexisting one. so just always heap-allocate and return, *but make the definition public* so we don't need to have two dozen functions accessing fields.

c++ can wrap this with an RAII using `nccapabilites_destroy()` as a custom deallocator, and i assume you can do something with rust to hide the dealloc. c users eat shit, as usual.
hrmmmmmmmmmmmmmmmmmmmm.

no sir, i don't like it. let's instead do:

```c
typedef struct {
} nccapabilites;
const struct nccapabilites* notcurses_...
const struct nccapabilites* ncdirect_...
```

and we will have a single `nccapabilites` struct inside the `notcurses` and `ncdirect` objects. we'll hand back a const reference. user is responsible for not using past the lifetime of the parent object. eliminates allocation and population cost, plus need to call destructor, at the cost of making possible unsafe late references. well, don't do that.
i've started this work in `dankamongmen/direct-capabilities`
`ncdirect_canopen_image()` already exists, btw

Fix patch:
diff --git a/include/notcurses/direct.h b/include/notcurses/direct.h
index d836c31e9a..db2dd3702e 100644
--- a/include/notcurses/direct.h
+++ b/include/notcurses/direct.h
@@ -238,16 +238,7 @@ API int ncdirect_clear(struct ncdirect* nc)
 API const char* ncdirect_detected_terminal(const struct ncdirect* n)
   __attribute__ ((nonnull (1)));
 
-// Can we load images? This requires being built against FFmpeg/OIIO.
-API bool ncdirect_canopen_images(const struct ncdirect* n)
-  __attribute__ ((nonnull (1)));
-
-// Is our encoding UTF-8? Requires LANG being set to a UTF8 locale.
-API bool ncdirect_canutf8(const struct ncdirect* n)
-  __attribute__ ((nonnull (1)));
-
-// Can we blit pixel-accurate bitmaps?
-API int ncdirect_check_pixel_support(const struct ncdirect* n)
+API const nccapabilities* ncdirect_capabilities(const struct ncdirect* n)
   __attribute__ ((nonnull (1)));
 
 // Draw horizontal/vertical lines using the specified channels, interpolating
@@ -425,6 +416,70 @@ API int ncdirect_stream(struct ncdirect* n, const char* filename, ncstreamcb str
                         struct ncvisual_options* vopts, void* curry)
   __attribute__ ((nonnull (1, 2)));
 
+// Capabilites
+
+API const char* ncdirect_detected_terminal(const struct ncdirect* nc)
+  __attribute__ ((nonnull (1)));
+
+// Can we directly specify RGB values per cell, or only use palettes?
+static inline bool
+ncdirect_cantruecolor(const struct ncdirect* nc){
+  return ncdirect_capabilities(nc)->rgb;
+}
+
+// Can we set the "hardware" palette? Requires the "ccc" terminfo capability.
+static inline bool
+ncdirect_canchangecolor(const struct ncdirect* nc){
+  return nccapability_canchangecolor(ncdirect_capabilities(nc));
+}
+
+// Can we fade? Fading requires either the "rgb" or "ccc" terminfo capability.
+static inline bool
+ncdirect_canfade(const struct ncdirect* nc){
+  return ncdirect_canchangecolor(nc) || ncdirect_cantruecolor(nc);
+}
+
+// Can we load images? This requires being built against FFmpeg/OIIO.
+API bool ncdirect_canopen_images(const struct ncdirect* n);
+
+// Can we load videos? This requires being built against FFmpeg.
+static inline bool
+ncdirect_canopen_videos(const struct ncdirect* nc __attribute__ ((unused))){
+  return notcurses_canopen_videos(NULL);
+}
+
+// Is our encoding UTF-8? Requires LANG being set to a UTF8 locale.
+API bool ncdirect_canutf8(const struct ncdirect* n)
+  __attribute__ ((nonnull (1)));
+
+// Can we blit pixel-accurate bitmaps?
+API int ncdirect_check_pixel_support(const struct ncdirect* n)
+  __attribute__ ((nonnull (1)));
+
+// Can we reliably use Unicode halfblocks?
+static inline bool
+ncdirect_canhalfblock(const struct ncdirect* nc){
+  return ncdirect_canutf8(nc);
+}
+
+// Can we reliably use Unicode quadrants?
+static inline bool
+ncdirect_canquadrant(const struct ncdirect* nc){
+  return ncdirect_canutf8(nc) && ncdirect_capabilities(nc)->quadrants;
+}
+
+// Can we reliably use Unicode 13 sextants?
+static inline bool
+ncdirect_cansextant(const struct ncdirect* nc){
+  return ncdirect_canutf8(nc) && ncdirect_capabilities(nc)->sextants;
+}
+
+// Can we reliably use Unicode Braille?
+static inline bool
+ncdirect_canbraille(const struct ncdirect* nc){
+  return ncdirect_canutf8(nc) && ncdirect_capabilities(nc)->braille;
+}
+
 #undef ALLOC
 #undef API
 
diff --git a/include/notcurses/notcurses.h b/include/notcurses/notcurses.h
index e1bf77f256..ad9f64f83c 100644
--- a/include/notcurses/notcurses.h
+++ b/include/notcurses/notcurses.h
@@ -1235,7 +1235,63 @@ API bool ncplane_translate_abs(const struct ncplane* n, int* RESTRICT y, int* RE
 // previously enabled, or false if it was disabled.
 API bool ncplane_set_scrolling(struct ncplane* n, bool scrollp);
 
-// Capabilities
+// Palette API. Some terminals only support 256 colors, but allow the full
+// palette to be specified with arbitrary RGB colors. In all cases, it's more
+// performant to use indexed colors, since it's much less data to write to the
+// terminal. If you can limit yourself to 256 colors, that's probably best.
+
+typedef struct ncpalette {
+  uint32_t chans[NCPALETTESIZE]; // RGB values as regular ol' channels
+} ncpalette;
+
+// Create a new palette store. It will be initialized with notcurses' best
+// knowledge of the currently configured palette. The palette upon startup
+// cannot be reliably detected, sadly.
+API ALLOC ncpalette* ncpalette_new(struct notcurses* nc);
+
+// Attempt to configure the terminal with the provided palette 'p'. Does not
+// transfer ownership of 'p'; palette256_free() can (ought) still be called.
+API int ncpalette_use(struct notcurses* nc, const ncpalette* p);
+
+// Manipulate entries in the palette store 'p'. These are *not* locked.
+static inline int
+ncpalette_set_rgb8(ncpalette* p, int idx, int r, int g, int b){
+  if(idx < 0 || (size_t)idx > sizeof(p->chans) / sizeof(*p->chans)){
+    return -1;
+  }
+  return ncchannel_set_rgb8(&p->chans[idx], r, g, b);
+}
+
+static inline int
+ncpalette_set(ncpalette* p, int idx, unsigned rgb){
+  if(idx < 0 || (size_t)idx > sizeof(p->chans) / sizeof(*p->chans)){
+    return -1;
+  }
+  return ncchannel_set(&p->chans[idx], rgb);
+}
+
+static inline int
+ncpalette_get_rgb8(const ncpalette* p, int idx, unsigned* RESTRICT r, unsigned* RESTRICT g, unsigned* RESTRICT b){
+  if(idx < 0 || (size_t)idx > sizeof(p->chans) / sizeof(*p->chans)){
+    return -1;
+  }
+  return ncchannel_rgb8(p->chans[idx], r, g, b);
+}
+
+// Free the palette store 'p'.
+API void ncpalette_free(ncpalette* p);
+
+// Capabilities, derived from terminfo, environment variables, and queries
+typedef struct nccapabilities {
+  unsigned colors;        // size of palette for indexed colors
+  bool utf8;              // are we using utf-8 encoding? from nl_langinfo(3)
+  bool rgb;               // 24bit color? COLORTERM/heuristics/terminfo 'rgb'
+  bool can_change_colors; // can we change the palette? terminfo 'ccc'
+  // these are assigned wholly through TERM- and query-based heuristics
+  bool quadrants; // do we have (good, vetted) Unicode 1 quadrant support?
+  bool sextants;  // do we have (good, vetted) Unicode 13 sextant support?
+  bool braille;   // do we have Braille support? (linux console does not)
+} nccapabilities;
 
 // Returns a 16-bit bitmask of supported curses-style attributes
 // (NCSTYLE_UNDERLINE, NCSTYLE_BOLD, etc.) The attribute is only
@@ -1261,7 +1317,21 @@ API bool notcurses_cantruecolor(const struct notcurses* nc)
 API bool notcurses_canfade(const struct notcurses* nc)
   __attribute__ ((nonnull (1)));
 
-// Can we set the "hardware" palette? Requires the "ccc" terminfo capability.
+// Can we set the "hardware" palette? Requires the "ccc" terminfo capability,
+// and that the number of colors supported is at least the size of our
+// ncpalette structure.
+static inline bool
+nccapability_canchangecolor(const nccapabilities* caps){
+  if(!caps->can_change_colors){
+    return false;
+  }
+  ncpalette* p;
+  if(caps->colors < sizeof(p->chans) / sizeof(*p->chans)){
+    return false;
+  }
+  return true;
+}
+
 API bool notcurses_canchangecolor(const struct notcurses* nc)
   __attribute__ ((nonnull (1)));
 
@@ -2962,52 +3032,6 @@ API int notcurses_cursor_yx(struct notcurses* nc, int* y, int* x);
 // cursor is already disabled.
 API int notcurses_cursor_disable(struct notcurses* nc);
 
-// Palette API. Some terminals only support 256 colors, but allow the full
-// palette to be specified with arbitrary RGB colors. In all cases, it's more
-// performant to use indexed colors, since it's much less data to write to the
-// terminal. If you can limit yourself to 256 colors, that's probably best.
-
-typedef struct ncpalette {
-  uint32_t chans[NCPALETTESIZE]; // RGB values as regular ol' channels
-} ncpalette;
-
-// Create a new palette store. It will be initialized with notcurses' best
-// knowledge of the currently configured palette. The palette upon startup
-// cannot be reliably detected, sadly.
-API ALLOC ncpalette* ncpalette_new(struct notcurses* nc);
-
-// Attempt to configure the terminal with the provided palette 'p'. Does not
-// transfer ownership of 'p'; palette256_free() can (ought) still be called.
-API int ncpalette_use(struct notcurses* nc, const ncpalette* p);
-
-// Manipulate entries in the palette store 'p'. These are *not* locked.
-static inline int
-ncpalette_set_rgb8(ncpalette* p, int idx, int r, int g, int b){
-  if(idx < 0 || (size_t)idx > sizeof(p->chans) / sizeof(*p->chans)){
-    return -1;
-  }
-  return ncchannel_set_rgb8(&p->chans[idx], r, g, b);
-}
-
-static inline int
-ncpalette_set(ncpalette* p, int idx, unsigned rgb){
-  if(idx < 0 || (size_t)idx > sizeof(p->chans) / sizeof(*p->chans)){
-    return -1;
-  }
-  return ncchannel_set(&p->chans[idx], rgb);
-}
-
-static inline int
-ncpalette_get_rgb8(const ncpalette* p, int idx, unsigned* RESTRICT r, unsigned* RESTRICT g, unsigned* RESTRICT b){
-  if(idx < 0 || (size_t)idx > sizeof(p->chans) / sizeof(*p->chans)){
-    return -1;
-  }
-  return ncchannel_rgb8(p->chans[idx], r, g, b);
-}
-
-// Free the palette store 'p'.
-API void ncpalette_free(ncpalette* p);
-
 // Convert the plane's content to greyscale.
 API void ncplane_greyscale(struct ncplane* n);
 
diff --git a/src/lib/blit.c b/src/lib/blit.c
index 9cb0282fd7..e47bbb66f7 100644
--- a/src/lib/blit.c
+++ b/src/lib/blit.c
@@ -934,7 +934,7 @@ const struct blitset* lookup_blitset(const tinfo* tcache, ncblitter_e setid, boo
     return NULL;
   }
   // without braille support, NCBLIT_BRAILLE decays to NCBLIT_3x2
-  if(!tcache->braille && setid == NCBLIT_BRAILLE){
+  if(!tcache->caps.braille && setid == NCBLIT_BRAILLE){
     if(may_degrade){
       setid = NCBLIT_3x2;
     }else{
@@ -950,7 +950,7 @@ const struct blitset* lookup_blitset(const tinfo* tcache, ncblitter_e setid, boo
     }
   }
   // without sextant support, NCBLIT_3x2 decays to NCBLIT_2x2
-  if(!tcache->sextants && setid == NCBLIT_3x2){
+  if(!tcache->caps.sextants && setid == NCBLIT_3x2){
     if(may_degrade){
       setid = NCBLIT_2x2;
     }else{
@@ -958,7 +958,7 @@ const struct blitset* lookup_blitset(const tinfo* tcache, ncblitter_e setid, boo
     }
   }
   // without quadrant support, NCBLIT_2x2 decays to NCBLIT_2x1
-  if(!tcache->quadrants && setid == NCBLIT_2x2){
+  if(!tcache->caps.quadrants && setid == NCBLIT_2x2){
     if(may_degrade){
       setid = NCBLIT_2x1;
     }else{
@@ -966,7 +966,7 @@ const struct blitset* lookup_blitset(const tinfo* tcache, ncblitter_e setid, boo
     }
   }
   // the only viable blitters in ASCII are NCBLIT_1x1 and NCBLIT_PIXEL
-  if(!tcache->utf8 && (setid != NCBLIT_1x1 && setid != NCBLIT_PIXEL)){
+  if(!tcache->caps.utf8 && (setid != NCBLIT_1x1 && setid != NCBLIT_PIXEL)){
     if(may_degrade){
       setid = NCBLIT_1x1;
     }else{
diff --git a/src/lib/blitset.h b/src/lib/blitset.h
index af241208c6..6980e3b897 100644
--- a/src/lib/blitset.h
+++ b/src/lib/blitset.h
@@ -25,16 +25,16 @@ encoding_x_scale(const tinfo* tcache, const struct blitset* bset) {
 // use NCBLIT_PIXEL for NCBLIT_DEFAULT, though maybe this ought change.
 static inline ncblitter_e 
 rgba_blitter_default(const tinfo* tcache, ncscale_e scale){
-  if(!tcache->utf8){
+  if(!tcache->caps.utf8){
     return NCBLIT_1x1; // only one that works in ASCII
   }
   if(scale == NCSCALE_NONE || scale == NCSCALE_SCALE){
     return NCBLIT_2x1;
   }
-  if(tcache->sextants){
+  if(tcache->caps.sextants){
     return NCBLIT_3x2;
   }
-  if(tcache->quadrants){
+  if(tcache->caps.quadrants){
     return NCBLIT_2x2;
   }
   return NCBLIT_2x1;
diff --git a/src/lib/debug.c b/src/lib/debug.c
index 2c33b1b8aa..85f527ddea 100644
--- a/src/lib/debug.c
+++ b/src/lib/debug.c
@@ -15,7 +15,7 @@ tinfo_debug_caps(const tinfo* ti, FILE* debugfp, int rows, int cols,
                  unsigned images, unsigned videos){
   const char indent[] = " ";
   fprintf(debugfp, "%scolors: %u rgb: %c ccc: %c setaf: %c setab: %c\n",
-          indent, ti->colors, capbool(ti->RGBflag), capbool(ti->CCCflag),
+          indent, ti->caps.colors, capbool(ti->caps.rgb), capbool(ti->caps.can_change_colors),
           capyn(get_escape(ti, ESCAPE_SETAF)),
           capyn(get_escape(ti, ESCAPE_SETAB)));
   fprintf(debugfp, "%ssgr: %c sgr0: %c op: %c fgop: %c bgop: %c\n",
@@ -37,10 +37,10 @@ tinfo_debug_caps(const tinfo* ti, FILE* debugfp, int rows, int cols,
     fprintf(debugfp, "%srgba pixel graphics supported\n", indent);
   }
   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),
+          indent, capbool(ti->caps.utf8), capbool(ti->caps.quadrants),
+          capbool(ti->caps.sextants), capbool(ti->caps.braille),
           capbool(images), capbool(videos));
-  if(ti->utf8){
+  if(ti->caps.utf8){
     fprintf(debugfp, "%s{%ls} {%ls} ⎧%.122ls⎫ ⎧%.6ls%.3ls⎫ ⎧%.6ls%.3ls⎫ ⎧█ ⎫ 🯰🯱\n", indent,
             get_blitter_egcs(NCBLIT_2x1), get_blitter_egcs(NCBLIT_2x2),
             get_blitter_egcs(NCBLIT_3x2),
diff --git a/src/lib/direct.c b/src/lib/direct.c
index 09d16c8d98..a4efc6cf47 100644
--- a/src/lib/direct.c
+++ b/src/lib/direct.c
@@ -981,7 +981,7 @@ int ncdirect_set_styles(ncdirect* n, unsigned stylebits){
 }
 
 unsigned ncdirect_palette_size(const ncdirect* nc){
-  return nc->tcache.colors;
+  return ncdirect_capabilities(nc)->colors;
 }
 
 int ncdirect_set_fg_default(ncdirect* nc){
@@ -1270,7 +1270,7 @@ bool ncdirect_canopen_images(const ncdirect* n __attribute__ ((unused))){
 
 // Is our encoding UTF-8? Requires LANG being set to a UTF8 locale.
 bool ncdirect_canutf8(const ncdirect* n){
-  return n->tcache.utf8;
+  return n->tcache.caps.utf8;
 }
 
 int ncdirect_flush(const ncdirect* nc){
@@ -1374,3 +1374,7 @@ unsigned ncdirect_supported_styles(const ncdirect* nc){
 const char* ncdirect_detected_terminal(const ncdirect* nc){
   return nc->tcache.termname;
 }
+
+const nccapabilities* ncdirect_capabilities(const ncdirect* n){
+  return &n->tcache.caps;
+}
diff --git a/src/lib/fade.c b/src/lib/fade.c
index f5b842b349..02cbcba73b 100644
--- a/src/lib/fade.c
+++ b/src/lib/fade.c
@@ -232,8 +232,8 @@ int ncplane_fadeout_iteration(ncplane* n, ncfadectx* nctx, int iter,
 
 static ncfadectx* 
 ncfadectx_setup_internal(ncplane* n, const struct timespec* ts){
-  if(!ncplane_notcurses(n)->tcache.RGBflag &&
-     !ncplane_notcurses(n)->tcache.CCCflag){ // terminal can't fade
+  if(!ncplane_notcurses(n)->tcache.caps.rgb &&
+     !ncplane_notcurses(n)->tcache.caps.can_change_colors){ // terminal can't fade
     return NULL;
   }
   ncfadectx* nctx = malloc(sizeof(*nctx));
@@ -301,8 +301,8 @@ int ncplane_fadein(ncplane* n, const struct timespec* ts, fadecb fader, void* cu
 int ncplane_pulse(ncplane* n, const struct timespec* ts, fadecb fader, void* curry){
   ncfadectx pp;
   int ret;
-  if(!ncplane_notcurses(n)->tcache.RGBflag &&
-     !ncplane_notcurses(n)->tcache.CCCflag){ // terminal can't fade
+  if(!ncplane_notcurses(n)->tcache.caps.rgb &&
+     !ncplane_notcurses(n)->tcache.caps.can_change_colors){ // terminal can't fade
     return -1;
   }
   if(alloc_ncplane_palette(n, &pp, ts)){
diff --git a/src/lib/notcurses.c b/src/lib/notcurses.c
index 908ac9c998..cd252141f7 100644
--- a/src/lib/notcurses.c
+++ b/src/lib/notcurses.c
@@ -831,13 +831,13 @@ init_banner_warnings(const notcurses* nc, FILE* out){
   // might be using stderr, so don't just reuse stdout decision
   const bool tty = isatty(fileno(out));
   if(tty){
-    term_fg_palindex(nc, out, nc->tcache.colors <= 88 ? 1 % nc->tcache.colors : 0xcb);
+    term_fg_palindex(nc, out, nc->tcache.caps.colors <= 88 ? 1 : 0xcb);
   }
-  if(!nc->tcache.RGBflag){ // FIXME
+  if(!nc->tcache.caps.rgb){
     fprintf(out, "\n Warning! Colors subject to https://github.com/dankamongmen/notcurses/issues/4");
     fprintf(out, "\n  Specify a (correct) TrueColor TERM, or COLORTERM=24bit.\n");
   }else{
-    if(!nc->tcache.CCCflag){
+    if(!nc->tcache.caps.can_change_colors){
       fprintf(out, "\n Warning! Advertised TrueColor but no 'ccc' flag\n");
     }
   }
@@ -861,24 +861,24 @@ static void
 init_banner(const notcurses* nc){
   if(!nc->suppress_banner){
     char prefixbuf[BPREFIXSTRLEN + 1];
-    term_fg_palindex(nc, stdout, 50 % nc->tcache.colors);
+    term_fg_palindex(nc, stdout, 50 % nc->tcache.caps.colors);
     printf("\n notcurses %s by nick black et al", notcurses_version());
     printf(" on %s", nc->tcache.termname ? nc->tcache.termname : "?");
-    term_fg_palindex(nc, stdout, 12 % nc->tcache.colors);
+    term_fg_palindex(nc, stdout, 12 % nc->tcache.caps.colors);
     if(nc->tcache.cellpixy && nc->tcache.cellpixx){
       printf("\n  %d rows (%dpx) %d cols (%dpx) (%sB) %zuB crend %d colors",
              nc->stdplane->leny, nc->tcache.cellpixy,
              nc->stdplane->lenx, nc->tcache.cellpixx,
              bprefix(nc->stats.fbbytes, 1, prefixbuf, 0),
-             sizeof(struct crender), nc->tcache.colors);
+             sizeof(struct crender), nc->tcache.caps.colors);
     }else{
       printf("\n  %d rows %d cols (%sB) %zuB crend %d colors",
              nc->stdplane->leny, nc->stdplane->lenx,
              bprefix(nc->stats.fbbytes, 1, prefixbuf, 0),
-             sizeof(struct crender), nc->tcache.colors);
+             sizeof(struct crender), nc->tcache.caps.colors);
     }
     const char* setaf;
-    if(nc->tcache.RGBflag && (setaf = get_escape(&nc->tcache, ESCAPE_SETAF))){
+    if(nc->tcache.caps.rgb && (setaf = get_escape(&nc->tcache, ESCAPE_SETAF))){
       putc('+', stdout);
       term_fg_rgb8(&nc->tcache, stdout, 0xe0, 0x60, 0x60);
       putc('R', stdout);
@@ -886,7 +886,8 @@ init_banner(const notcurses* nc){
       putc('G', stdout);
       term_fg_rgb8(&nc->tcache, stdout, 0x20, 0x80, 0xff);
       putc('B', stdout);
-      term_fg_palindex(nc, stdout, nc->tcache.colors <= 256 ? 12 % nc->tcache.colors : 0x2080e0);
+      term_fg_palindex(nc, stdout, nc->tcache.caps.colors <= 256 ?
+                       12 % nc->tcache.caps.colors : 0x2080e0);
     }
     printf("\n  compiled with gcc-%s, %zuB %s-endian cells\n"
            "  terminfo from %s\n",
@@ -1634,7 +1635,7 @@ unsigned notcurses_supported_styles(const notcurses* nc){
 }
 
 unsigned notcurses_palette_size(const notcurses* nc){
-  return nc->tcache.colors;
+  return nc->tcache.caps.colors;
 }
 
 const char* notcurses_detected_terminal(const notcurses* nc){
@@ -1642,7 +1643,7 @@ const char* notcurses_detected_terminal(const notcurses* nc){
 }
 
 bool notcurses_cantruecolor(const notcurses* nc){
-  return nc->tcache.RGBflag;
+  return nc->tcache.caps.rgb;
 }
 
 // conform to the specified stylebits
@@ -2126,38 +2127,31 @@ int notcurses_mouse_disable(notcurses* n){
 }
 
 bool notcurses_canutf8(const notcurses* nc){
-  return nc->tcache.utf8;
+  return nc->tcache.caps.utf8;
 }
 
 bool notcurses_canhalfblock(const notcurses* nc){
-  return nc->tcache.utf8;
+  return nc->tcache.caps.utf8;
 }
 
 bool notcurses_canquadrant(const notcurses* nc){
-  return nc->tcache.quadrants && nc->tcache.utf8;
+  return nc->tcache.caps.quadrants && nc->tcache.caps.utf8;
 }
 
 bool notcurses_cansextant(const notcurses* nc){
-  return nc->tcache.sextants && nc->tcache.utf8;
+  return nc->tcache.caps.sextants && nc->tcache.caps.utf8;
 }
 
 bool notcurses_canbraille(const notcurses* nc){
-  return nc->tcache.braille && nc->tcache.utf8;
+  return nc->tcache.caps.braille && nc->tcache.caps.utf8;
 }
 
 bool notcurses_canfade(const notcurses* nc){
-  return nc->tcache.CCCflag || nc->tcache.RGBflag;
+  return nc->tcache.caps.can_change_colors || nc->tcache.caps.rgb;
 }
 
 bool notcurses_canchangecolor(const notcurses* nc){
-  if(!nc->tcache.CCCflag){
-    return false;
-  }
-  ncpalette* p;
-  if((unsigned)nc->tcache.colors < sizeof(p->chans) / sizeof(*p->chans)){
-    return false;
-  }
-  return true;
+  return nccapability_canchangecolor(&nc->tcache.caps);
 }
 
 ncpalette* ncpalette_new(notcurses* nc){
diff --git a/src/lib/render.c b/src/lib/render.c
index cc79b11a1b..271921f4f5 100644
--- a/src/lib/render.c
+++ b/src/lib/render.c
@@ -701,7 +701,7 @@ term_bg_rgb8(const tinfo* ti, FILE* out, unsigned r, unsigned g, unsigned b){
   // etc. For the case of DirectColor, there is no suitable terminfo entry, but
   // we're also in that case working with hopefully more robust terminals.
   // If it doesn't work, eh, it doesn't work. Fuck the world; save yourself.
-  if(ti->RGBflag){
+  if(ti->caps.rgb){
     if(ti->bg_collides_default){
       if((r == (ti->bg_collides_default & 0xff0000lu)) &&
          (g == (ti->bg_collides_default & 0xff00lu)) &&
@@ -721,9 +721,9 @@ term_bg_rgb8(const tinfo* ti, FILE* out, unsigned r, unsigned g, unsigned b){
       // the inputs *if we can change the palette*. If more than 256 are used on
       // a single screen, start... combining close ones? For 8-color mode, simple
       // interpolation. I have no idea what to do for 88 colors. FIXME
-      if(ti->colors >= 256){
+      if(ti->caps.colors >= 256){
         return term_emit(tiparm(setab, rgb_quantize_256(r, g, b)), out, false);
-      }else if(ti->colors >= 8){
+      }else if(ti->caps.colors >= 8){
         return term_emit(tiparm(setab, rgb_quantize_8(r, g, b)), out, false);
       }
     }
@@ -737,7 +737,7 @@ int term_fg_rgb8(const tinfo* ti, FILE* out, unsigned r, unsigned g, unsigned b)
   // etc. For the case of DirectColor, there is no suitable terminfo entry, but
   // we're also in that case working with hopefully more robust terminals.
   // If it doesn't work, eh, it doesn't work. Fuck the world; save yourself.
-  if(ti->RGBflag){
+  if(ti->caps.rgb){
     return term_esc_rgb(out, true, r, g, b);
   }else{
     const char* setaf = get_escape(ti, ESCAPE_SETAF);
@@ -746,9 +746,9 @@ int term_fg_rgb8(const tinfo* ti, FILE* out, unsigned r, unsigned g, unsigned b)
       // the inputs *if we can change the palette*. If more than 256 are used on
       // a single screen, start... combining close ones? For 8-color mode, simple
       // interpolation. I have no idea what to do for 88 colors. FIXME
-      if(ti->colors >= 256){
+      if(ti->caps.colors >= 256){
         return term_emit(tiparm(setaf, rgb_quantize_256(r, g, b)), out, false);
-      }else if(ti->colors >= 8){
+      }else if(ti->caps.colors >= 8){
         return term_emit(tiparm(setaf, rgb_quantize_8(r, g, b)), out, false);
       }
     }
@@ -758,7 +758,7 @@ int term_fg_rgb8(const tinfo* ti, FILE* out, unsigned r, unsigned g, unsigned b)
 
 static inline int
 update_palette(notcurses* nc, FILE* out){
-  if(nc->tcache.CCCflag){
+  if(nc->tcache.caps.can_change_colors){
     const char* initc = get_escape(&nc->tcache, ESCAPE_INITC);
     for(size_t damageidx = 0 ; damageidx < sizeof(nc->palette.chans) / sizeof(*nc->palette.chans) ; ++damageidx){
       unsigned r, g, b;
diff --git a/src/lib/termdesc.c b/src/lib/termdesc.c
index b65e7f69b8..33c2909a23 100644
--- a/src/lib/termdesc.c
+++ b/src/lib/termdesc.c
@@ -96,59 +96,59 @@ apply_term_heuristics(tinfo* ti, const char* termname, int fd,
   if(qterm == TERMINAL_UNKNOWN){
     match_termname(termname, &qterm);
   }
-  // st had neithersextants nor quadrants last i checked (0.8.4)
-  ti->braille = true; // most everyone has working braille, even from fonts
+  // st had neithercaps.sextants nor caps.quadrants last i checked (0.8.4)
+  ti->caps.braille = true; // most everyone has working caps.braille, even from fonts
   if(qterm == TERMINAL_KITTY){ // kitty (https://sw.kovidgoyal.net/kitty/)
     termname = "Kitty";
     // see https://sw.kovidgoyal.net/kitty/protocol-extensions.html
     ti->bg_collides_default |= 0x1000000;
-    ti->sextants = true; // work since bugfix in 0.19.3
-    ti->quadrants = true;
-    ti->RGBflag = true;
+    ti->caps.sextants = true; // work since bugfix in 0.19.3
+    ti->caps.quadrants = true;
+    ti->caps.rgb = true;
     setup_kitty_bitmaps(ti, fd);
   }else if(qterm == TERMINAL_ALACRITTY){
     termname = "Alacritty";
-    ti->quadrants = true;
-    // ti->sextants = true; // alacritty https://github.com/alacritty/alacritty/issues/4409 */
-    ti->RGBflag = true;
+    ti->caps.quadrants = true;
+    // ti->caps.sextants = true; // alacritty https://github.com/alacritty/alacritty/issues/4409 */
+    ti->caps.rgb = true;
   }else if(qterm == TERMINAL_VTE){
     termname = "VTE";
-    ti->quadrants = true;
-    ti->sextants = true; // VTE has long enjoyed good sextant support
+    ti->caps.quadrants = true;
+    ti->caps.sextants = true; // VTE has long enjoyed good sextant support
   }else if(qterm == TERMINAL_FOOT){
     termname = "foot";
-    ti->sextants = true;
-    ti->quadrants = true;
-    ti->RGBflag = true;
+    ti->caps.sextants = true;
+    ti->caps.quadrants = true;
+    ti->caps.rgb = true;
   }else if(qterm == TERMINAL_MLTERM){
     termname = "MLterm";
-    ti->quadrants = true; // good quadrants, no sextants as of 3.9.0
+    ti->caps.quadrants = true; // good caps.quadrants, no caps.sextants as of 3.9.0
     ti->sprixel_cursor_hack = true;
   }else if(qterm == TERMINAL_WEZTERM){
     termname = "WezTerm";
-    ti->quadrants = true;
+    ti->caps.quadrants = true;
     // FIXME get version from query
     const char* termver = getenv("TERM_PROGRAM_VERSION");
     if(termver && strcmp(termver, "20210610") >= 0){
-      ti->sextants = true; // good sextants as of 2021-06-10
+      ti->caps.sextants = true; // good caps.sextants as of 2021-06-10
     }
   }else if(qterm == TERMINAL_XTERM){
     termname = "XTerm";
   }else if(strcmp(termname, "linux") == 0){
     termname = "Linux console";
-    ti->braille = false; // no braille, no sextants in linux console
+    ti->caps.braille = false; // no caps.braille, no caps.sextants in linux console
     // FIXME if the NCOPTION_NO_FONT_CHANGES, this isn't true
     // FIXME we probably want to do this based off ioctl()s in linux.c
     // FIXME until #1726 is fixed this definitely is not happening
-    ti->quadrants = false; // we program quadrants on the console
+    ti->caps.quadrants = false; // we program caps.quadrants on the console
   }
   // run a wcwidth(⣿) to guarantee libc Unicode 3 support, independent of term
   if(wcwidth(L'⣿') < 0){
-    ti->braille = false;
+    ti->caps.braille = false;
   }
   // run a wcwidth(🬸) to guarantee libc Unicode 13 support, independent of term
   if(wcwidth(L'🬸') < 0){
-    ti->sextants = false;
+    ti->caps.sextants = false;
   }
   ti->termname = termname;
   return 0;
@@ -262,17 +262,17 @@ int interrogate_terminfo(tinfo* ti, int fd, const char* termname, unsigned utf8,
       return -1;
     }
   }
-  ti->utf8 = utf8;
+  ti->caps.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.
   int colors = tigetnum("colors");
   if(colors <= 0){
-    ti->colors = 1;
+    ti->caps.colors = 1;
   }else{
-    ti->colors = colors;
+    ti->caps.colors = colors;
   }
-  ti->RGBflag = query_rgb(); // independent of colors
+  ti->caps.rgb = query_rgb(); // independent of colors
   // verify that the terminal provides cursor addressing (absolute movement)
   const struct strtdesc {
     escape_e esc;
@@ -324,7 +324,7 @@ int interrogate_terminfo(tinfo* ti, int fd, const char* termname, unsigned utf8,
   if(colors){
     const char* initc = get_escape(ti, ESCAPE_INITC);
     if(initc){
-      ti->CCCflag = tigetflag("ccc") == 1;
+      ti->caps.can_change_colors = tigetflag("ccc") == 1;
     }
   }else{ // disable initc if there's no color support
     ti->escindices[ESCAPE_INITC] = 0;
diff --git a/src/lib/termdesc.h b/src/lib/termdesc.h
index 11eefc2d9f..0cef053861 100644
--- a/src/lib/termdesc.h
+++ b/src/lib/termdesc.h
@@ -94,7 +94,7 @@ typedef struct ncinputlayer {
 typedef struct tinfo {
   uint16_t escindices[ESCAPE_MAX]; // table of 1-biased indices into esctable
   char* esctable;                  // packed table of escape sequences
-  unsigned colors;// number of colors terminfo reported usable for this screen
+  nccapabilities caps;             // exported to the user, when requested
   // we use the cell's size in pixels for pixel blitting. this information can
   // be acquired on all terminals with pixel support.
   int cellpixy;   // cell pixel height, might be 0
@@ -132,19 +132,9 @@ 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 RGBflag;   // "RGB" flag for 24bpc truecolor
-  bool CCCflag;   // "CCC" flag for palette set capability
   bool BCEflag;   // "BCE" flag for erases with background color
   bool AMflag;    // "AM" flag for automatic movement to next line
 
-  // assigned based off nl_langinfo() in notcurses_core_init()
-  bool utf8;      // are we using utf-8 encoding, as hoped?
-
-  // these are assigned wholly through TERM-based heuristics
-  bool quadrants; // do we have (good, vetted) Unicode 1 quadrant support?
-  bool sextants;  // do we have (good, vetted) Unicode 13 sextant support?
-  bool braille;   // do we have Braille support? (linux console does not)
-
   // 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)


Test patch:
diff --git a/src/tests/blit.cpp b/src/tests/blit.cpp
index 7eab964875..8a2179f401 100644
--- a/src/tests/blit.cpp
+++ b/src/tests/blit.cpp
@@ -113,7 +113,7 @@ TEST_CASE("Blit") {
   // original version https://github.com/dankamongmen/notcurses/issues/1354
   SUBCASE("QuadblitterMax") {
     if(notcurses_canutf8(nc_)){
-      if(nc_->tcache.quadrants){
+      if(notcurses_canquadrant(nc_)){
         uint32_t p2x2[4];
         uint32_t* ptl = &p2x2[0];
         ncpixel_set_a(ptl, 0xff);
diff --git a/src/tests/stacking.cpp b/src/tests/stacking.cpp
index ea5493714a..e976ad360d 100644
--- a/src/tests/stacking.cpp
+++ b/src/tests/stacking.cpp
@@ -107,7 +107,7 @@ TEST_CASE("Stacking") {
   }
 
   SUBCASE("StackedQuadHalves") {
-    if(nc_->tcache.quadrants){
+    if(notcurses_canquadrant(nc_)){
       struct ncplane_options opts = {
         0, 0, 1, 1, nullptr, "top", nullptr, 0, 0, 0,
       };
@@ -147,7 +147,7 @@ TEST_CASE("Stacking") {
   }
 
   SUBCASE("StackedQuadCrossed") {
-    if(nc_->tcache.quadrants){
+    if(notcurses_canquadrant(nc_)){
       ncplane_erase(n_);
       notcurses_refresh(nc_, nullptr, nullptr);
       struct ncplane_options opts = {
diff --git a/src/tests/visual.cpp b/src/tests/visual.cpp
index d0a675e27a..d2d01ca104 100644
--- a/src/tests/visual.cpp
+++ b/src/tests/visual.cpp
@@ -264,128 +264,167 @@ TEST_CASE("Visual") {
 
   // write a checkerboard pattern and verify the NCBLIT_2x2 output
   SUBCASE("Quadblitter") {
-    if(notcurses_canutf8(nc_)){
-      if(nc_->tcache.quadrants){
-        constexpr int DIMY = 10;
-        constexpr int DIMX = 11; // odd number to get checkerboard effect
-        auto rgba = new uint32_t[DIMY * DIMX];
-        for(int i = 0 ; i < DIMY * DIMX ; ++i){
-          CHECK(0 == ncpixel_set_a(&rgba[i], 0xff));
-          if(i % 2){
-            CHECK(0 == ncpixel_set_b(&rgba[i], 0xff));
-            CHECK(0 == ncpixel_set_g(&rgba[i], 0));
-          }else{
-            CHECK(0 == ncpixel_set_g(&rgba[i], 0xff));
-            CHECK(0 == ncpixel_set_b(&rgba[i], 0));
-          }
-          CHECK(0 == ncpixel_set_r(&rgba[i], 0));
+    if(notcurses_canquadrant(nc_)){
+      constexpr int DIMY = 10;
+      constexpr int DIMX = 11; // odd number to get checkerboard effect
+      auto rgba = new uint32_t[DIMY * DIMX];
+      for(int i = 0 ; i < DIMY * DIMX ; ++i){
+        CHECK(0 == ncpixel_set_a(&rgba[i], 0xff));
+        if(i % 2){
+          CHECK(0 == ncpixel_set_b(&rgba[i], 0xff));
+          CHECK(0 == ncpixel_set_g(&rgba[i], 0));
+        }else{
+          CHECK(0 == ncpixel_set_g(&rgba[i], 0xff));
+          CHECK(0 == ncpixel_set_b(&rgba[i], 0));
         }
-        auto ncv = ncvisual_from_rgba(rgba, DIMY, DIMX * sizeof(uint32_t), DIMX);
-        REQUIRE(nullptr != ncv);
-        struct ncvisual_options vopts{};
-        vopts.n = n_;
-        vopts.blitter = NCBLIT_2x2;
-        vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-        CHECK(n_ == ncvisual_render(nc_, ncv, &vopts));
-        CHECK(0 == notcurses_render(nc_));
-        for(int y = 0 ; y < DIMY / 2 ; ++y){
-          for(int x = 0 ; x < DIMX / 2 ; ++x){
-            uint16_t stylemask;
-            uint64_t channels;
-            char* egc = notcurses_at_yx(nc_, y, x, &stylemask, &channels);
-            REQUIRE(nullptr != egc);
-            CHECK((htole(rgba[(y * 2 * DIMX) + (x * 2)]) & 0xffffff) == ncchannels_fg_rgb(channels));
-            CHECK((htole(rgba[(y * 2 + 1) * DIMX + (x * 2) + 1]) & 0xffffff) == ncchannels_fg_rgb(channels));
-            free(egc);
-          }
+        CHECK(0 == ncpixel_set_r(&rgba[i], 0));
+      }
+      auto ncv = ncvisual_from_rgba(rgba, DIMY, DIMX * sizeof(uint32_t), DIMX);
+      REQUIRE(nullptr != ncv);
+      struct ncvisual_options vopts{};
+      vopts.n = n_;
+      vopts.blitter = NCBLIT_2x2;
+      vopts.flags = NCVISUAL_OPTION_NODEGRADE;
+      CHECK(n_ == ncvisual_render(nc_, ncv, &vopts));
+      CHECK(0 == notcurses_render(nc_));
+      for(int y = 0 ; y < DIMY / 2 ; ++y){
+        for(int x = 0 ; x < DIMX / 2 ; ++x){
+          uint16_t stylemask;
+          uint64_t channels;
+          char* egc = notcurses_at_yx(nc_, y, x, &stylemask, &channels);
+          REQUIRE(nullptr != egc);
+          CHECK((htole(rgba[(y * 2 * DIMX) + (x * 2)]) & 0xffffff) == ncchannels_fg_rgb(channels));
+          CHECK((htole(rgba[(y * 2 + 1) * DIMX + (x * 2) + 1]) & 0xffffff) == ncchannels_fg_rgb(channels));
+          free(egc);
         }
-        delete[] rgba;
       }
+      delete[] rgba;
     }
   }
 
   // close-in verification of each quadblitter output EGC 
   SUBCASE("QuadblitterEGCs") {
-    if(notcurses_canutf8(nc_)){
-      if(nc_->tcache.quadrants){
-        // there are 16 configurations, each mapping four (2x2) pixels
-        int DIMX = 32;
-        int DIMY = 2;
-        auto rgba = new uint32_t[DIMY * DIMX];
-        memset(rgba, 0, sizeof(*rgba) * DIMY * DIMX);
-        // the top has 4 configurations of 4 each, each being 2 columns
-        for(int top = 0 ; top < 4 ; ++top){
-          for(int idx = 0 ; idx < 4 ; ++idx){
-            const int itop = (top * 4 + idx) * 2; // index of first column
-            CHECK(0 == ncpixel_set_a(&rgba[itop], 0xff));
-            CHECK(0 == ncpixel_set_a(&rgba[itop + 1], 0xff));
-            if(top == 1 || top == 3){
-              CHECK(0 == ncpixel_set_r(&rgba[itop], 0xff));
-            }
-            if(top == 2 || top == 3){
-              CHECK(0 == ncpixel_set_r(&rgba[itop + 1], 0xff));
-            }
+    if(notcurses_canquadrant(nc_)){
+      // there are 16 configurations, each mapping four (2x2) pixels
+      int DIMX = 32;
+      int DIMY = 2;
+      auto rgba = new uint32_t[DIMY * DIMX];
+      memset(rgba, 0, sizeof(*rgba) * DIMY * DIMX);
+      // the top has 4 configurations of 4 each, each being 2 columns
+      for(int top = 0 ; top < 4 ; ++top){
+        for(int idx = 0 ; idx < 4 ; ++idx){
+          const int itop = (top * 4 + idx) * 2; // index of first column
+          CHECK(0 == ncpixel_set_a(&rgba[itop], 0xff));
+          CHECK(0 == ncpixel_set_a(&rgba[itop + 1], 0xff));
+          if(top == 1 || top == 3){
+            CHECK(0 == ncpixel_set_r(&rgba[itop], 0xff));
           }
-        }
-        for(int bot = 0 ; bot < 4 ; ++bot){
-          for(int idx = 0 ; idx < 4 ; ++idx){
-            const int ibot = (bot * 4 + idx) * 2 + DIMX;
-            CHECK(0 == ncpixel_set_a(&rgba[ibot], 0xff));
-            CHECK(0 == ncpixel_set_a(&rgba[ibot + 1], 0xff));
-            if(idx == 1 || idx == 3){
-              CHECK(0 == ncpixel_set_r(&rgba[ibot], 0xff));
-            }
-            if(idx == 2 || idx == 3){
-              CHECK(0 == ncpixel_set_r(&rgba[ibot + 1], 0xff));
-            }
+          if(top == 2 || top == 3){
+            CHECK(0 == ncpixel_set_r(&rgba[itop + 1], 0xff));
           }
         }
-        auto ncv = ncvisual_from_rgba(rgba, DIMY, DIMX * sizeof(uint32_t), DIMX);
-        REQUIRE(nullptr != ncv);
-        struct ncvisual_options vopts{};
-        vopts.n = n_;
-        vopts.blitter = NCBLIT_2x2;
-        vopts.flags = NCVISUAL_OPTION_NODEGRADE;
-        CHECK(n_ == ncvisual_render(nc_, ncv, &vopts));
-        CHECK(0 == notcurses_render(nc_));
-        for(int y = 0 ; y < DIMY / 2 ; ++y){
-          for(int x = 0 ; x < DIMX / 2 ; ++x){
-            uint16_t stylemask;
-            uint64_t channels;
-            char* egc = notcurses_at_yx(nc_, y, x, &stylemask, &channels);
-            REQUIRE(nullptr != egc);
-    /* FIXME need to match
-    [▀] 00000000 00000000
-    [▜] 00000000 00ff0000
-    [▛] 00000000 00ff0000
-    [▀] 00000000 00ff0000
-    [▟] 00000000 00ff0000
-    [▋] 00ff0000 00000000
-    [▚] 00ff0000 00000000
-    [▙] 00ff0000 00000000
-    [▙] 00000000 00ff0000
-    [▚] 00000000 00ff0000
-    [▋] 00000000 00ff0000
-    [▟] 00ff0000 00000000
-    [▀] 00ff0000 00000000
-    [▛] 00ff0000 00000000
-    [▜] 00ff0000 00000000
-    [▀] 00ff0000 00ff0000
-    */
-            free(egc);
+      }
+      for(int bot = 0 ; bot < 4 ; ++bot){
+        for(int idx = 0 ; idx < 4 ; ++idx){
+          const int ibot = (bot * 4 + idx) * 2 + DIMX;
+          CHECK(0 == ncpixel_set_a(&rgba[ibot], 0xff));
+          CHECK(0 == ncpixel_set_a(&rgba[ibot + 1], 0xff));
+          if(idx == 1 || idx == 3){
+            CHECK(0 == ncpixel_set_r(&rgba[ibot], 0xff));
+          }
+          if(idx == 2 || idx == 3){
+            CHECK(0 == ncpixel_set_r(&rgba[ibot + 1], 0xff));
           }
         }
-        delete[] rgba;
       }
+      auto ncv = ncvisual_from_rgba(rgba, DIMY, DIMX * sizeof(uint32_t), DIMX);
+      REQUIRE(nullptr != ncv);
+      struct ncvisual_options vopts{};
+      vopts.n = n_;
+      vopts.blitter = NCBLIT_2x2;
+      vopts.flags = NCVISUAL_OPTION_NODEGRADE;
+      CHECK(n_ == ncvisual_render(nc_, ncv, &vopts));
+      CHECK(0 == notcurses_render(nc_));
+      for(int y = 0 ; y < DIMY / 2 ; ++y){
+        for(int x = 0 ; x < DIMX / 2 ; ++x){
+          uint16_t stylemask;
+          uint64_t channels;
+          char* egc = notcurses_at_yx(nc_, y, x, &stylemask, &channels);
+          REQUIRE(nullptr != egc);
+  /* FIXME need to match
+  [▀] 00000000 00000000
+  [▜] 00000000 00ff0000
+  [▛] 00000000 00ff0000
+  [▀] 00000000 00ff0000
+  [▟] 00000000 00ff0000
+  [▋] 00ff0000 00000000
+  [▚] 00ff0000 00000000
+  [▙] 00ff0000 00000000
+  [▙] 00000000 00ff0000
+  [▚] 00000000 00ff0000
+  [▋] 00000000 00ff0000
+  [▟] 00ff0000 00000000
+  [▀] 00ff0000 00000000
+  [▛] 00ff0000 00000000
+  [▜] 00ff0000 00000000
+  [▀] 00ff0000 00ff0000
+  */
+          free(egc);
+        }
+      }
+      delete[] rgba;
     }
   }
 
   // quadblitter with all 4 colors equal ought generate space
   SUBCASE("Quadblitter4Same") {
-    if(notcurses_canutf8(nc_)){
-      if(nc_->tcache.quadrants){
-        const uint32_t pixels[4] = { htole(0xff605040), htole(0xff605040), htole(0xff605040), htole(0xff605040) };
-        auto ncv = ncvisual_from_rgba(pixels, 2, 2 * sizeof(*pixels), 2);
+    if(notcurses_canquadrant(nc_)){
+      const uint32_t pixels[4] = { htole(0xff605040), htole(0xff605040), htole(0xff605040), htole(0xff605040) };
+      auto ncv = ncvisual_from_rgba(pixels, 2, 2 * sizeof(*pixels), 2);
+      REQUIRE(nullptr != ncv);
+      struct ncvisual_options vopts = {
+        .n = nullptr,
+        .scaling = NCSCALE_NONE,
+        .y = 0,
+        .x = 0,
+        .begy = 0,
+        .begx = 0,
+        .leny = 0,
+        .lenx = 0,
+        .blitter = NCBLIT_2x2,
+        .flags = 0,
+        .transcolor = 0,
+      };
+      auto ncvp = ncvisual_render(nc_, ncv, &vopts);
+      REQUIRE(nullptr != ncvp);
+      int dimy, dimx;
+      ncplane_dim_yx(ncvp, &dimy, &dimx);
+      CHECK(1 == dimy);
+      CHECK(1 == dimx);
+      uint16_t stylemask;
+      uint64_t channels;
+      auto egc = ncplane_at_yx(ncvp, 0, 0, &stylemask, &channels);
+      CHECK(0 == strcmp(" ", egc));
+      CHECK(0 == stylemask);
+      CHECK(0x405060 == ncchannels_fg_rgb(channels));
+      CHECK(0x405060 == ncchannels_bg_rgb(channels));
+      free(egc);
+      ncvisual_destroy(ncv);
+      CHECK(0 == notcurses_render(nc_));
+    }
+  }
+
+  // quadblitter with three pixels equal ought generate three-quarter block
+  SUBCASE("Quadblitter3Same") {
+    if(notcurses_canquadrant(nc_)){
+      const uint32_t pixels[4][4] = {
+        { htole(0xffcccccc), htole(0xff605040), htole(0xff605040), htole(0xff605040) },
+        { htole(0xff605040), htole(0xffcccccc), htole(0xff605040), htole(0xff605040) },
+        { htole(0xff605040), htole(0xff605040), htole(0xffcccccc), htole(0xff605040) },
+        { htole(0xff605040), htole(0xff605040), htole(0xff605040), htole(0xffcccccc) } };
+      const char* egcs[] = { "▟", "▙", "▜", "▛" };
+      for(int i = 0 ; i < 4 ; ++i){
+        auto ncv = ncvisual_from_rgba(pixels[i], 2, 2 * sizeof(**pixels), 2);
         REQUIRE(nullptr != ncv);
         struct ncvisual_options vopts = {
           .n = nullptr,
@@ -409,10 +448,10 @@ TEST_CASE("Visual") {
         uint16_t stylemask;
         uint64_t channels;
         auto egc = ncplane_at_yx(ncvp, 0, 0, &stylemask, &channels);
-        CHECK(0 == strcmp(" ", egc));
+        CHECK(0 == strcmp(egcs[i], egc));
         CHECK(0 == stylemask);
         CHECK(0x405060 == ncchannels_fg_rgb(channels));
-        CHECK(0x405060 == ncchannels_bg_rgb(channels));
+        CHECK(0xcccccc == ncchannels_bg_rgb(channels));
         free(egc);
         ncvisual_destroy(ncv);
         CHECK(0 == notcurses_render(nc_));
@@ -420,205 +459,152 @@ TEST_CASE("Visual") {
     }
   }
 
-  // quadblitter with three pixels equal ought generate three-quarter block
-  SUBCASE("Quadblitter3Same") {
-    if(notcurses_canutf8(nc_)){
-      if(nc_->tcache.quadrants){
-        const uint32_t pixels[4][4] = {
-          { htole(0xffcccccc), htole(0xff605040), htole(0xff605040), htole(0xff605040) },
-          { htole(0xff605040), htole(0xffcccccc), htole(0xff605040), htole(0xff605040) },
-          { htole(0xff605040), htole(0xff605040), htole(0xffcccccc), htole(0xff605040) },
-          { htole(0xff605040), htole(0xff605040), htole(0xff605040), htole(0xffcccccc) } };
-        const char* egcs[] = { "▟", "▙", "▜", "▛" };
-        for(int i = 0 ; i < 4 ; ++i){
-          auto ncv = ncvisual_from_rgba(pixels[i], 2, 2 * sizeof(**pixels), 2);
-          REQUIRE(nullptr != ncv);
-          struct ncvisual_options vopts = {
-            .n = nullptr,
-            .scaling = NCSCALE_NONE,
-            .y = 0,
-            .x = 0,
-            .begy = 0,
-            .begx = 0,
-            .leny = 0,
-            .lenx = 0,
-            .blitter = NCBLIT_2x2,
-            .flags = 0,
-            .transcolor = 0,
-          };
-          auto ncvp = ncvisual_render(nc_, ncv, &vopts);
-          REQUIRE(nullptr != ncvp);
-          int dimy, dimx;
-          ncplane_dim_yx(ncvp, &dimy, &dimx);
-          CHECK(1 == dimy);
-          CHECK(1 == dimx);
-          uint16_t stylemask;
-          uint64_t channels;
-          auto egc = ncplane_at_yx(ncvp, 0, 0, &stylemask, &channels);
-          CHECK(0 == strcmp(egcs[i], egc));
-          CHECK(0 == stylemask);
-          CHECK(0x405060 == ncchannels_fg_rgb(channels));
-          CHECK(0xcccccc == ncchannels_bg_rgb(channels));
-          free(egc);
-          ncvisual_destroy(ncv);
-          CHECK(0 == notcurses_render(nc_));
-        }
-      }
-    }
-  }
-
   // quadblitter with two sets of two equal pixels
   SUBCASE("Quadblitter2Pairs") {
-    if(notcurses_canutf8(nc_)){
-      if(nc_->tcache.quadrants){
-        const uint32_t pixels[6][4] = {
-          { htole(0xffcccccc), htole(0xffcccccc), htole(0xff605040), htole(0xff605040) },
-          { htole(0xffcccccc), htole(0xff605040), htole(0xffcccccc), htole(0xff605040) },
-          { htole(0xffcccccc), htole(0xff605040), htole(0xff605040), htole(0xffcccccc) },
-          { htole(0xff605040), htole(0xffcccccc), htole(0xffcccccc), htole(0xff605040) },
-          { htole(0xff605040), htole(0xffcccccc), htole(0xff605040), htole(0xffcccccc) },
-          { htole(0xff605040), htole(0xff605040), htole(0xffcccccc), htole(0xffcccccc) } };
-        const char* egcs[] = { "▀", "▌", "▚", "▚", "▌", "▀" };
-        for(size_t i = 0 ; i < sizeof(egcs) / sizeof(*egcs) ; ++i){
-          auto ncv = ncvisual_from_rgba(pixels[i], 2, 2 * sizeof(**pixels), 2);
-          REQUIRE(nullptr != ncv);
-          struct ncvisual_options vopts = {
-            .n = nullptr,
-            .scaling = NCSCALE_NONE,
-            .y = 0,
-            .x = 0,
-            .begy = 0,
-            .begx = 0,
-            .leny = 0,
-            .lenx = 0,
-            .blitter = NCBLIT_2x2,
-            .flags = 0,
-            .transcolor = 0,
-          };
-          auto ncvp = ncvisual_render(nc_, ncv, &vopts);
-          REQUIRE(nullptr != ncvp);
-          int dimy, dimx;
-          ncplane_dim_yx(ncvp, &dimy, &dimx);
-          CHECK(1 == dimy);
-          CHECK(1 == dimx);
-          uint16_t stylemask;
-          uint64_t channels;
-          auto egc = ncplane_at_yx(ncvp, 0, 0, &stylemask, &channels);
-          CHECK(0 == strcmp(egcs[i], egc));
-          CHECK(0 == stylemask);
-          if(i >= 3){
-            CHECK(0x405060 == ncchannels_fg_rgb(channels));
-            CHECK(0xcccccc == ncchannels_bg_rgb(channels));
-          }else{
-            CHECK(0x405060 == ncchannels_bg_rgb(channels));
-            CHECK(0xcccccc == ncchannels_fg_rgb(channels));
-          }
-          free(egc);
-          ncvisual_destroy(ncv);
-          CHECK(0 == notcurses_render(nc_));
+    if(notcurses_canquadrant(nc_)){
+      const uint32_t pixels[6][4] = {
+        { htole(0xffcccccc), htole(0xffcccccc), htole(0xff605040), htole(0xff605040) },
+        { htole(0xffcccccc), htole(0xff605040), htole(0xffcccccc), htole(0xff605040) },
+        { htole(0xffcccccc), htole(0xff605040), htole(0xff605040), htole(0xffcccccc) },
+        { htole(0xff605040), htole(0xffcccccc), htole(0xffcccccc), htole(0xff605040) },
+        { htole(0xff605040), htole(0xffcccccc), htole(0xff605040), htole(0xffcccccc) },
+        { htole(0xff605040), htole(0xff605040), htole(0xffcccccc), htole(0xffcccccc) } };
+      const char* egcs[] = { "▀", "▌", "▚", "▚", "▌", "▀" };
+      for(size_t i = 0 ; i < sizeof(egcs) / sizeof(*egcs) ; ++i){
+        auto ncv = ncvisual_from_rgba(pixels[i], 2, 2 * sizeof(**pixels), 2);
+        REQUIRE(nullptr != ncv);
+        struct ncvisual_options vopts = {
+          .n = nullptr,
+          .scaling = NCSCALE_NONE,
+          .y = 0,
+          .x = 0,
+          .begy = 0,
+          .begx = 0,
+          .leny = 0,
+          .lenx = 0,
+          .blitter = NCBLIT_2x2,
+          .flags = 0,
+          .transcolor = 0,
+        };
+        auto ncvp = ncvisual_render(nc_, ncv, &vopts);
+        REQUIRE(nullptr != ncvp);
+        int dimy, dimx;
+        ncplane_dim_yx(ncvp, &dimy, &dimx);
+        CHECK(1 == dimy);
+        CHECK(1 == dimx);
+        uint16_t stylemask;
+        uint64_t channels;
+        auto egc = ncplane_at_yx(ncvp, 0, 0, &stylemask, &channels);
+        CHECK(0 == strcmp(egcs[i], egc));
+        CHECK(0 == stylemask);
+        if(i >= 3){
+          CHECK(0x405060 == ncchannels_fg_rgb(channels));
+          CHECK(0xcccccc == ncchannels_bg_rgb(channels));
+        }else{
+          CHECK(0x405060 == ncchannels_bg_rgb(channels));
+          CHECK(0xcccccc == ncchannels_fg_rgb(channels));
         }
+        free(egc);
+        ncvisual_destroy(ncv);
+        CHECK(0 == notcurses_render(nc_));
       }
     }
   }
 
   // quadblitter with one pair plus two split
   SUBCASE("Quadblitter1Pair") {
-    if(notcurses_canutf8(nc_)){
-      if(nc_->tcache.quadrants){
-        const uint32_t pixels[6][4] = {
-          { htole(0xffcccccc), htole(0xff444444), htole(0xff605040), htole(0xff605040) },
-          { htole(0xff444444), htole(0xff605040), htole(0xffcccccc), htole(0xff605040) },
-          { htole(0xffcccccc), htole(0xff605040), htole(0xff605040), htole(0xff444444) },
-          { htole(0xff605040), htole(0xffcccccc), htole(0xff444444), htole(0xff605040) },
-          { htole(0xff605040), htole(0xffeeeeee), htole(0xff605040), htole(0xffcccccc) },
-          { htole(0xff605040), htole(0xff605040), htole(0xffeeeeee), htole(0xffcccccc) } };
-        const char* egcs[] = { "▟", "▜", "▟", "▙", "▌", "▀" };
-        for(size_t i = 0 ; i < sizeof(egcs) / sizeof(*egcs) ; ++i){
-          auto ncv = ncvisual_from_rgba(pixels[i], 2, 2 * sizeof(**pixels), 2);
-          REQUIRE(nullptr != ncv);
-          struct ncvisual_options vopts = {
-            .n = nullptr,
-            .scaling = NCSCALE_NONE,
-            .y = 0,
-            .x = 0,
-            .begy = 0,
-            .begx = 0,
-            .leny = 0,
-            .lenx = 0,
-            .blitter = NCBLIT_2x2,
-            .flags = 0,
-            .transcolor = 0,
-          };
-          auto ncvp = ncvisual_render(nc_, ncv, &vopts);
-          REQUIRE(nullptr != ncvp);
-          int dimy, dimx;
-          ncplane_dim_yx(ncvp, &dimy, &dimx);
-          CHECK(1 == dimy);
-          CHECK(1 == dimx);
-          uint16_t stylemask;
-          uint64_t channels;
-          auto egc = ncplane_at_yx(ncvp, 0, 0, &stylemask, &channels);
-          CHECK(0 == strcmp(egcs[i], egc));
-          CHECK(0 == stylemask);
-          if(i > 3){
-            CHECK(0x405060 == ncchannels_fg_rgb(channels));
-            CHECK(0xdddddd == ncchannels_bg_rgb(channels));
-          }else{
-            CHECK(0x424c57 == ncchannels_fg_rgb(channels));
-            CHECK(0xcccccc == ncchannels_bg_rgb(channels));
-          }
-          free(egc);
-          ncvisual_destroy(ncv);
-          CHECK(0 == notcurses_render(nc_));
+    if(notcurses_canquadrant(nc_)){
+      const uint32_t pixels[6][4] = {
+        { htole(0xffcccccc), htole(0xff444444), htole(0xff605040), htole(0xff605040) },
+        { htole(0xff444444), htole(0xff605040), htole(0xffcccccc), htole(0xff605040) },
+        { htole(0xffcccccc), htole(0xff605040), htole(0xff605040), htole(0xff444444) },
+        { htole(0xff605040), htole(0xffcccccc), htole(0xff444444), htole(0xff605040) },
+        { htole(0xff605040), htole(0xffeeeeee), htole(0xff605040), htole(0xffcccccc) },
+        { htole(0xff605040), htole(0xff605040), htole(0xffeeeeee), htole(0xffcccccc) } };
+      const char* egcs[] = { "▟", "▜", "▟", "▙", "▌", "▀" };
+      for(size_t i = 0 ; i < sizeof(egcs) / sizeof(*egcs) ; ++i){
+        auto ncv = ncvisual_from_rgba(pixels[i], 2, 2 * sizeof(**pixels), 2);
+        REQUIRE(nullptr != ncv);
+        struct ncvisual_options vopts = {
+          .n = nullptr,
+          .scaling = NCSCALE_NONE,
+          .y = 0,
+          .x = 0,
+          .begy = 0,
+          .begx = 0,
+          .leny = 0,
+          .lenx = 0,
+          .blitter = NCBLIT_2x2,
+          .flags = 0,
+          .transcolor = 0,
+        };
+        auto ncvp = ncvisual_render(nc_, ncv, &vopts);
+        REQUIRE(nullptr != ncvp);
+        int dimy, dimx;
+        ncplane_dim_yx(ncvp, &dimy, &dimx);
+        CHECK(1 == dimy);
+        CHECK(1 == dimx);
+        uint16_t stylemask;
+        uint64_t channels;
+        auto egc = ncplane_at_yx(ncvp, 0, 0, &stylemask, &channels);
+        CHECK(0 == strcmp(egcs[i], egc));
+        CHECK(0 == stylemask);
+        if(i > 3){
+          CHECK(0x405060 == ncchannels_fg_rgb(channels));
+          CHECK(0xdddddd == ncchannels_bg_rgb(channels));
+        }else{
+          CHECK(0x424c57 == ncchannels_fg_rgb(channels));
+          CHECK(0xcccccc == ncchannels_bg_rgb(channels));
         }
+        free(egc);
+        ncvisual_destroy(ncv);
+        CHECK(0 == notcurses_render(nc_));
       }
     }
   }
 
   // quadblitter with one pair plus two split
   SUBCASE("QuadblitterAllDifferent") {
-    if(notcurses_canutf8(nc_)){
-      if(nc_->tcache.quadrants){
-        const uint32_t pixels[6][4] = {
-          { htole(0xffdddddd), htole(0xff000000), htole(0xff111111), htole(0xff222222) },
-          { htole(0xff000000), htole(0xff111111), htole(0xffdddddd), htole(0xff222222) },
-          { htole(0xff111111), htole(0xffdddddd), htole(0xff000000), htole(0xff222222) },
-          { htole(0xff000000), htole(0xffcccccc), htole(0xff222222), htole(0xffeeeeee) },
-          { htole(0xff222222), htole(0xff000000), htole(0xffeeeeee), htole(0xffcccccc), } };
-        const char* egcs[] = { "▟", "▜", "▙", "▌", "▀" };
-        for(size_t i = 0 ; i < sizeof(egcs) / sizeof(*egcs) ; ++i){
-          auto ncv = ncvisual_from_rgba(pixels[i], 2, 2 * sizeof(**pixels), 2);
-          REQUIRE(nullptr != ncv);
-          struct ncvisual_options vopts = {
-            .n = nullptr,
-            .scaling = NCSCALE_NONE,
-            .y = 0,
-            .x = 0,
-            .begy = 0,
-            .begx = 0,
-            .leny = 0,
-            .lenx = 0,
-            .blitter = NCBLIT_2x2,
-            .flags = 0,
-            .transcolor = 0,
-          };
-          auto ncvp = ncvisual_render(nc_, ncv, &vopts);
-          REQUIRE(nullptr != ncvp);
-          int dimy, dimx;
-          ncplane_dim_yx(ncvp, &dimy, &dimx);
-          CHECK(1 == dimy);
-          CHECK(1 == dimx);
-          uint16_t stylemask;
-          uint64_t channels;
-          auto egc = ncplane_at_yx(ncvp, 0, 0, &stylemask, &channels);
-          CHECK(0 == strcmp(egcs[i], egc));
-          CHECK(0 == stylemask);
-          CHECK(0x111111 == ncchannels_fg_rgb(channels));
-          CHECK(0xdddddd == ncchannels_bg_rgb(channels));
-          free(egc);
-          ncvisual_destroy(ncv);
-          CHECK(0 == notcurses_render(nc_));
-        }
+    if(notcurses_canquadrant(nc_)){
+      const uint32_t pixels[6][4] = {
+        { htole(0xffdddddd), htole(0xff000000), htole(0xff111111), htole(0xff222222) },
+        { htole(0xff000000), htole(0xff111111), htole(0xffdddddd), htole(0xff222222) },
+        { htole(0xff111111), htole(0xffdddddd), htole(0xff000000), htole(0xff222222) },
+        { htole(0xff000000), htole(0xffcccccc), htole(0xff222222), htole(0xffeeeeee) },
+        { htole(0xff222222), htole(0xff000000), htole(0xffeeeeee), htole(0xffcccccc), } };
+      const char* egcs[] = { "▟", "▜", "▙", "▌", "▀" };
+      for(size_t i = 0 ; i < sizeof(egcs) / sizeof(*egcs) ; ++i){
+        auto ncv = ncvisual_from_rgba(pixels[i], 2, 2 * sizeof(**pixels), 2);
+        REQUIRE(nullptr != ncv);
+        struct ncvisual_options vopts = {
+          .n = nullptr,
+          .scaling = NCSCALE_NONE,
+          .y = 0,
+          .x = 0,
+          .begy = 0,
+          .begx = 0,
+          .leny = 0,
+          .lenx = 0,
+          .blitter = NCBLIT_2x2,
+          .flags = 0,
+          .transcolor = 0,
+        };
+        auto ncvp = ncvisual_render(nc_, ncv, &vopts);
+        REQUIRE(nullptr != ncvp);
+        int dimy, dimx;
+        ncplane_dim_yx(ncvp, &dimy, &dimx);
+        CHECK(1 == dimy);
+        CHECK(1 == dimx);
+        uint16_t stylemask;
+        uint64_t channels;
+        auto egc = ncplane_at_yx(ncvp, 0, 0, &stylemask, &channels);
+        CHECK(0 == strcmp(egcs[i], egc));
+        CHECK(0 == stylemask);
+        CHECK(0x111111 == ncchannels_fg_rgb(channels));
+        CHECK(0xdddddd == ncchannels_bg_rgb(channels));
+        free(egc);
+        ncvisual_destroy(ncv);
+        CHECK(0 == notcurses_render(nc_));
       }
     }
   }