0
\$\begingroup\$

C++ is not aware of the console and it's behaviour / features / options, only its std::cout stream. To do more than stream we need to be platform aware.

Aim

To conveniently write console applications which use "clear screen" and "cursor position" in a way that works correctly across *nix and Windows (MacOS is extra, but probably much the same as *nix).

The same function with simple signature should be callable for each feature and the correct cross-platform code will be compiled. No #ifdef _WIN32 in main code.

Questions

  • Feedback on code style / API / encapsulation and abstractions. The functionalily is clearly very simple.
  • "Most appropriate" error handling technique? Return bool for success from each API function? (see TODOs)
  • Once fleshed out, is this actually useful? Is there some public library available that I have missed?

Known TODOs

  • Expand functionality to colours and other display attributes
  • Error handling in the win32 code

includes/xos/console.hpp

namespace xos::console {

void move_cursor_relative(int dx, int dy);
void clear_screen();
void move_cursor_absolute(int x, int y);

} // namespace xos::console

includes/xos/console.cpp

#include "console.hpp"
#include <iostream>
#include <string_view>

#ifdef _WIN32
#include <windows.h>
#endif
namespace xos::console {

namespace impl {

namespace ansi {
// ANSI Control sequences for *nix consoles
const std::string_view csi             = "\x1B["; // "Control Sequence Introducer"
const std::string_view cursor_up       = "A";
const std::string_view cursor_down     = "B";
const std::string_view cursor_forward  = "C";
const std::string_view cursor_backward = "D";

const std::string_view clear_screen = "2J";
const std::string_view home         = "H";
const std::string_view cursor_pos   = "H";

} // namespace ansi

// only try to compile the non-standard win32 code on windows
#ifdef _WIN32

void win32_move_cursor_relative(int dx, int dy) {
  HANDLE                     console = GetStdHandle(STD_OUTPUT_HANDLE);
  CONSOLE_SCREEN_BUFFER_INFO screen;
  GetConsoleScreenBufferInfo(console, &screen);
  COORD cursorPos = screen.dwCursorPosition;
  cursorPos.X += dx;
  cursorPos.Y += dy;
  SetConsoleCursorPosition(console, cursorPos);
}

void win32_console_clear_screen() {
  COORD                      topLeft = {0, 0};
  HANDLE                     console = GetStdHandle(STD_OUTPUT_HANDLE);
  CONSOLE_SCREEN_BUFFER_INFO screen;
  DWORD                      written;

  GetConsoleScreenBufferInfo(console, &screen);
  FillConsoleOutputCharacterA(console, ' ', screen.dwSize.X * screen.dwSize.Y, topLeft, &written);
  FillConsoleOutputAttribute(console, FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_BLUE,
                             screen.dwSize.X * screen.dwSize.Y, topLeft, &written);
  SetConsoleCursorPosition(console, topLeft);
}

#endif

void nix_move_cursor_relative(int dx, int dy) {
  if (dx != 0) {
    std::cout << ansi::csi;
    if (dx > 0)
      std::cout << dx << ansi::cursor_forward;
    else
      std::cout << -dx << ansi::cursor_backward;
  }

  if (dy != 0) {
    std::cout << ansi::csi;
    if (dy > 0)
      std::cout << dy << ansi::cursor_down; // +ve == down for consistency with win32
    else
      std::cout << -dy << ansi::cursor_up;
  }
}

} // namespace impl

/**
 * @brief      platform independent console cursor relative movement
 * @details    works on *nix, macOS and Windows
 * @param      int dx +ve is right
 * @param      int dy +ve is down
 */
void move_cursor_relative(int dx, int dy) {
#ifdef _WIN32
  impl::win32_move_cursor_relative(dx, dy);
#else
  impl::nix_move_cursor_relative(dx, dy);
#endif
}

/**
 * @brief      platform independent console cursor absolute movement
 * @details    works on *nix, macOS and Windows
 * @param      int x 0 is top leftmost column
 * @param      int y 0 is top row
 */
void move_cursor_absolute(int x, int y) {
#ifdef _WIN32
  HANDLE console   = GetStdHandle(STD_OUTPUT_HANDLE);
  COORD  cursorPos = {x, y};
  SetConsoleCursorPosition(console, cursorPos);
#else
  // ansi code is 1 based - stick to zero based for consistency
  std::cout << impl::ansi::csi << (y + 1) << ';' << (x + 1) << impl::ansi::cursor_pos;
#endif
}

/**
 * @brief      platform independent console clear screen
 * @details    clear visible console (not scrollback) and moves home (1, 1)
 */
void clear_screen() {
#ifdef _WIN32
  impl::win32_console_clear_screen();
#else
  std::cout << impl::ansi::csi << impl::ansi::clear_screen;
  std::cout << impl::ansi::csi << impl::ansi::home;
#endif
}

} // namespace xos::console

main.cpp

#include xos/console.hpp
#include <iostream>

int main() {
  xos::console::clear_screen();
  xos::console::move_cursor_relative(2, 2); // relative(2 right, 2 down)
  std::cout << "hello";
  xos::console::move_cursor_absolute(20, 4); // @(20 right, 4 down)
  std::cout << "there";

  return EXIT_SUCCESS;
}
\$\endgroup\$
8
  • \$\begingroup\$ hmm, Windows version requires console creation, no? Also this really could benefit from defunctionalization for sake of reducing number of calls. \$\endgroup\$
    – Swift
    Commented Dec 24, 2020 at 6:33
  • \$\begingroup\$ Not sure what you mean on console creation. It runs fine on windows and *nix for me. Is MSVC hiding some magic from me? Re call graph: I thought about that, but went for readibility, given any I/O is super slow anyway and the compiler should inline redundant calls for me? \$\endgroup\$ Commented Dec 24, 2020 at 10:01
  • 2
    \$\begingroup\$ Is Curses or Termcap not already available on the platforms you care about? Even if not, I recommend using it where available, so you don't need to inspect $TERM to choose the correct escape sequences yourself. Remember that console need not be a local terminal! \$\endgroup\$ Commented Dec 24, 2020 at 10:32
  • \$\begingroup\$ redunant api call won't be optimized, they are calls to .dll with side-effects on each. The console would work only if app is run from console. If you wan't it be standalone, you have to create console window (certain types of VS projects elide that requirement). Note, only one active console window per process is allowed \$\endgroup\$
    – Swift
    Commented Dec 25, 2020 at 17:04
  • \$\begingroup\$ @Swift I think the double call within the object file (or .dll if you want to call it that) will be inlined with any decent compiler. The external call will not, but that's always the case? Will look at the VS magic that's obviously going on (I know almost nothing about it). \$\endgroup\$ Commented Dec 25, 2020 at 18:14

1 Answer 1

2
\$\begingroup\$

There's an excellent article about clearing the screen on cplusplus.com. It looks like what you are doing is what they recommend, if clearing the screen and moving the cursor are the only two things you want to do. Otherwise, one should use an existing library like (a modern version of) curses.

Answers to your questions

Feedback on code style / API / encapsulation and abstractions. The functionalily is clearly very simple.

The API is fine. However, I don't particularly think the namespaces are done right or used consistently. Why is move_cursor_relative() calling impl:: functions, but move_cursor_absolute() is doing things directly?

I would rather organize the code like so:

namespace impl {
#ifdef _WIN32
void console_clear_screen() {/* Windows implementation here */}
void move_cursor_absolute(int x, int y) {...}
void move_cursor_relative(int dx, int dy) {...}
#else
void console_clear_screen() {/* Unix implementation here */}
void move_cursor_absolute(int x, int y) {...}
void move_cursor_relative(int dx, int dy) {...}
#endif
}

Or just forget about namespace impl, and put all the code directly into the API functions inside console.cpp, since I don't think the impl namespace is adding any value here. Maybe if the code gets more complex, but until then: YAGNI.

Note that I do like that you put the API in its own namespace, this avoids polluting the global namespace of course, and users can always do using namespace xos::console if they want.

"Most appropriate" error handling technique? Return bool for success from each API function? (see TODOs)

You could indeed add a bool return value, or consider the fact that not being able to output to the screen is so exceptional that you should... throw an exception. As a user, I probably won't care about the screen not clearing properly, by that time there are bigger problems. I wouldn't want to add error handling for each call to these functions. The only time I want to be really conscious about this is when you really want to clear the screen because there is some sensitive information on it, and then you don't want the error condition to be accidentily ignored. So I would lean towards using exceptions here.

Once fleshed out, is this actually useful? Is there some public library available that I have missed?

Yes, this is very useful, and in fact there are the various curses libraries that are out there that solve exactly this issue, as well as the colour and display attributes mentioned in your list of TODOs, and many more things.

\$\endgroup\$
13
  • \$\begingroup\$ Thanks. I am aware of ncurses and have used it. Very old style C-API. Is there a "properly modern" one? Like actually C++? Also ncurses may be overkill for the more minimalist use-case I had in mind. I did debate the namespace hierarchy, and appreciate your thoughts. \$\endgroup\$ Commented Dec 24, 2020 at 11:28
  • \$\begingroup\$ You say "minimalist use-case" but then have colour and attributes in your TODO :) Ncurses comes with a C++ interface, although it's just slapping classes on top of the C API as expected. A quick search shows that there are several C++ wrappers around ncurses out there, this one seems like a good one. \$\endgroup\$
    – G. Sliepen
    Commented Dec 24, 2020 at 11:41
  • 1
    \$\begingroup\$ Ncurses has the panels, forms and menus in separate .so files. \$\endgroup\$
    – G. Sliepen
    Commented Dec 24, 2020 at 13:11
  • 1
    \$\begingroup\$ For sure their API is the same for the standard curses functionality, but I'm not sure about the ABI. \$\endgroup\$
    – G. Sliepen
    Commented Dec 24, 2020 at 14:08
  • 2
    \$\begingroup\$ @G.Sliepen you don't need to bother about ABI cross-platform. Each platform got own ABI anyway (especially the unique snowflake Windows). That's why defunctionalization can be preferred - create adapters between your API and various library ones \$\endgroup\$
    – Swift
    Commented Dec 25, 2020 at 17:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.