Re: [PATCH v9 0/9] Input/Output Terminal Abstraction
Grant Erickson <[email protected]> Tue, 27 May 2025 11:08:06 -0700
| Newsgroups | dev.linux.lists.ell |
|---|---|
| Message-ID | <[email protected]> |
On May 27, 2025, at 10:20=E2=80=AFAM, Marcel Holtmann = <[email protected]> wrote: >=20 >=20 > Hi Grant, >=20 >> This expands on Marcel Holtman's 2023-12-22 RFCv4 patch for an >> input/output terminal abstraction. >>=20 >> Substantive changes from the v7 version: >>=20 >> * Fixed a typo in the ASCII C0 and C1 control code mnemonics. >>=20 >> Grant Erickson (9): >> term: Initial revision. >=20 > I actually reverted it back to my version and included printf / = vprintf helpers. >=20 > What is the purpose of l_term_set_bounds? Is it important for non-TTY = setups since for TTY compatible terminals, this should not be needed. It=E2=80=99s been a year and a half since I first deploy this, so bear = with me as I attempt to clear some dust and cobwebs from some of the = details. Regarding l_term_set_bounds, It was a function that I appeared to have = used during early prototyping but never ended up leveraging in this = implementation wrapper: status_t Terminal :: SetBounds( const uint16_t &inRows, const uint16_t &inColumns ) { status_t lRetval =3D STATUS_SUCCESS; nlEXPECT_ACTION(mEllTerminal !=3D nullptr, done, lRetval =3D ERROR_NOT_INITIALIZED); lRetval =3D l_term_set_bounds(mEllTerminal.get(), inRows, inColumns); done: return (lRetval); } I suspect it was just a symmetric peer to my GetBounds observation = method: status_t Terminal :: GetBounds( uint16_t &outRows, uint16_t &outColumns ) const { status_t lRetval =3D STATUS_SUCCESS; nlEXPECT_ACTION(mEllTerminal !=3D nullptr, done, lRetval =3D ERROR_NOT_INITIALIZED); lRetval =3D l_term_get_rows(mEllTerminal.get(), &outRows); nlREQUIRE_SUCCESS(lRetval, done); lRetval =3D l_term_get_columns(mEllTerminal.get(), &outColumns); nlREQUIRE_SUCCESS(lRetval, done); done: return (lRetval); } > The l_term_io_callback and l_term_process is something I don=E2=80=99t = really like since I rather keep that internal. The implementation needs run loop adaptation. Absent those functions, = what would you propose instead? My initializer, deinitializer, and run loop adaptation: status_t Terminal :: Init( FILE *inInput, FILE *inOutput ) { status_t lRetval =3D STATUS_SUCCESS; nlREQUIRE_ACTION(inInput !=3D nullptr, done, lRetval =3D = -EINVAL); nlREQUIRE_ACTION(inOutput !=3D nullptr, done, lRetval =3D = -EINVAL); lRetval =3D Init(fileno(inInput), fileno(inOutput)); done: return (lRetval); } status_t Terminal :: Init( const os_descriptor_t &inInput, const os_descriptor_t &inOutput ) { status_t lRetval =3D STATUS_SUCCESS; lRetval =3D osDescriptorValidate(inInput); nlREQUIRE_SUCCESS(lRetval, done); lRetval =3D osDescriptorValidate(inOutput); nlREQUIRE_SUCCESS(lRetval, done); mEllTerminal.reset(l_term_new()); nlREQUIRE_ACTION(mEllTerminal !=3D nullptr, done, lRetval =3D = -ENOMEM); lRetval =3D l_term_set_io_handler(mEllTerminal.get(), = IoCallback, this); nlREQUIRE_SUCCESS(lRetval, done); lRetval =3D l_term_set_key_handler(mEllTerminal.get(), = KeyCallback, this); nlREQUIRE_SUCCESS(lRetval, done); lRetval =3D l_term_set_input(mEllTerminal.get(), inInput); nlREQUIRE_SUCCESS(lRetval, done); lRetval =3D l_term_set_output(mEllTerminal.get(), inOutput); nlREQUIRE_SUCCESS(lRetval, done); done: if (lRetval < STATUS_SUCCESS) { if (mEllTerminal !=3D nullptr) { l_term_set_key_handler(mEllTerminal.get(), nullptr, = nullptr); l_term_set_io_handler(mEllTerminal.get(), nullptr, = nullptr); mEllTerminal.reset(); } } return (lRetval); } status_t Terminal :: Shutdown(void) { status_t lRetval =3D STATUS_SUCCESS; nlEXPECT(mEllTerminal !=3D nullptr, done); if (IsAcquired()) { Release(); } l_term_set_key_handler(mEllTerminal.get(), nullptr, = nullptr); l_term_set_io_handler(mEllTerminal.get(), nullptr, nullptr); mEllTerminal.reset(); done: return (lRetval); } // MARK: Introspection bool Terminal :: IsAcquired(void) const { bool lRetval =3D false; nlEXPECT(mEllTerminal !=3D nullptr, done); lRetval =3D l_term_is_acquired(mEllTerminal.get()); done: return (lRetval); } void Terminal :: IoCallback( const os_descriptor_t &inDescriptor, const bool &inReadable, const bool &inWritable ) { nlREQUIRE(osDescriptorIsValid(inDescriptor), done); if (inReadable) { mDescriptorEventMap[inDescriptor] |=3D POLLIN; } else { mDescriptorEventMap[inDescriptor] &=3D ~POLLIN; } if (inWritable) { mDescriptorEventMap[inDescriptor] |=3D POLLOUT; } else { mDescriptorEventMap[inDescriptor] &=3D ~POLLOUT; } done: return; } status_t Terminal :: GetDescriptors( os_descriptor_t &inOutHighestDescriptor, fd_set &inOutReadDescriptors, fd_set &inOutWriteDescriptors, fd_set &inOutExceptionDescriptors ) const { DescriptorEventMap::const_iterator lDescriptorEventMapCurrent =3D= mDescriptorEventMap.cbegin(); DescriptorEventMap::const_iterator lDescriptorEventMapLast =3D= mDescriptorEventMap.cend(); status_t lRetval =3D= STATUS_SUCCESS; while (lDescriptorEventMapCurrent !=3D lDescriptorEventMapLast) { // Simply handle the input descriptor. We assume that the // output descriptor is always ready for writing. if ((lDescriptorEventMapCurrent->second & POLLIN) && osDescriptorIsValid(lDescriptorEventMapCurrent->first)) { FD_SET(lDescriptorEventMapCurrent->first, = &inOutReadDescriptors); inOutHighestDescriptor =3D = max(inOutHighestDescriptor, lDescriptorEventMapCurrent->first); } advance(lDescriptorEventMapCurrent, 1); } return (lRetval); } status_t Terminal :: HandleDescriptors( fd_set &inOutReadDescriptors, fd_set &inOutWriteDescriptors, fd_set &inOutExceptionDescriptors ) { DescriptorEventMap::iterator lDescriptorEventMapCurrent =3D mDescriptorEventMap.begin(); DescriptorEventMap::iterator lDescriptorEventMapLast =3D mDescriptorEventMap.end(); status_t lRetval =3D STATUS_SUCCESS; while (lDescriptorEventMapCurrent !=3D lDescriptorEventMapLast) { if (FD_ISSET(lDescriptorEventMapCurrent->first, = &inOutReadDescriptors)) { l_term_process(mEllTerminal.get()); } advance(lDescriptorEventMapCurrent, 1); } return (lRetval); } There=E2=80=99s a similar set of methods to adapt it to poll rather than = select; however, I=E2=80=99ve elided those since you can more or less = guess how they are implemented. >> ell: Add include directive for 'ell/term.h'. >> ell/Makefile: Added 'term.[ch]' to HEADERS and SOURCES. >> term: Added 'l_term_*' symbols. >=20 > These 3 were already in my latest set. Acknowledged; I started with your patch set as a baseline so that = reviewers could see the entire context given that your patches were not = yet in-tree. >=20 >> ell/term: Do not return -EPERM for 'putnstr' and 'vprint' if not >> running. >=20 > Why is that important. It seems like a hack. If you haven=E2=80=99t = successfully acquired the terminal, there is no point in writing to it = since you have no idea what termios setting are dealing with. Per the above, regrettably, at this point I don=E2=80=99t recall the = issue that I had run into. For what they are worth, my output methods = are: status_t Terminal :: Write( const char &inCharacter ) { status_t lRetval =3D STATUS_SUCCESS; nlEXPECT_ACTION(mEllTerminal !=3D nullptr, done, lRetval =3D ERROR_NOT_INITIALIZED); lRetval =3D l_term_putchar(mEllTerminal.get(), inCharacter); done: return (lRetval); } status_t Terminal :: Write( const char *inString ) { status_t lRetval =3D STATUS_SUCCESS; nlEXPECT_ACTION(mEllTerminal !=3D nullptr, done, lRetval =3D ERROR_NOT_INITIALIZED); lRetval =3D l_term_putstr(mEllTerminal.get(), inString); done: return (lRetval); } status_t Terminal :: Write( const char *inString, const size_t &inStringLength ) { status_t lRetval =3D STATUS_SUCCESS; nlEXPECT_ACTION(mEllTerminal !=3D nullptr, done, lRetval =3D ERROR_NOT_INITIALIZED); lRetval =3D l_term_putnstr(mEllTerminal.get(), inString, inStringLength); done: return (lRetval); } status_t Terminal :: WriteWithFormat( const char *inFormat, ... ) { va_list lArguments; status_t lRetval =3D STATUS_SUCCESS; va_start(lArguments, inFormat); lRetval =3D WriteWithFormat(inFormat, lArguments); va_end(lArguments); return (lRetval); } status_t Terminal :: WriteWithFormat( const char *inFormat, va_list inArguments ) { status_t lRetval =3D STATUS_SUCCESS; nlEXPECT_ACTION(mEllTerminal !=3D nullptr, done, lRetval =3D ERROR_NOT_INITIALIZED); lRetval =3D l_term_vprint(mEllTerminal.get(), inFormat, inArguments); done: return (lRetval); } >> ell/term: Return error on writes if the output descriptor is invalid. >=20 > Seems like a fix for the previous change. It might well be. Again, per the comment above about getting back into = the context of a year and a half ago, it seems like there was a desire = to be able to initialize an instance with the stdin / stdout file stream = pointers and have things work as-is and then bootstrap into / out of = those with the terminal as my line oriented UI/UX acquired / released = the terminal as UI/UX elements were pushed/popped off the stack. >> ell/edit: Rename 'l_term_{open,close}'. >=20 > I incorporated that into my set. Makes sense to me. Re-reviewing this particular patch and recollecting the acquire/release = push/pop semantics around the UI/UX stack and stdin/stdout, I believe = that starts to get at the -EPERM / -EBADF changes. Otherwise, I think = the UI/UX stack would have effectively had to toggle in / toggle out a = =E2=80=9CNULL Terminal=E2=80=9D as the actual terminal was acquired / = released. >> ell/term: Add an 'l_term_is_acquired' introspection function. >=20 > I added it, but don=E2=80=99t really know how it would be used. See above use case in the =E2=80=98Shutdown=E2=80=99 deinitializer. >> ell/term: Added ASCII C0 and C1 control code mnemonics. >=20 > Hmm. We could do that, but the constant name is too long for my taste. = And why use an enum instead of defines? enumerations seemed to be the prevailing style in ELL. Best, Grant --=20 Principal Nuovations [email protected] https://www.nuovations.com/