The program running in a console decides what appears in that console


James Risto asks, "Is there a way to change the behavior of the CMD.EXE window? I would like to add a status line."

The use of the phrase "the CMD.EXE window" is ambiguous. James could be referring to the console itself, or he could be referring to the CMD.EXE progarm.

The program running in a console decides what appears in the console. If you want to devote a line of text to a status bar, then feel free to code one up. But if you didn't write the program that's running, then you're at the mercy of whatever that program decided to display.

Just to show that it can be done, here's a totally useless console program that contains a status bar.

#define UNICODE
#define _UNICODE
#include <windows.h>
#include <strsafe.h> // for StringCchPrintf

void DrawStatusBar(HANDLE hScreen)
{
 CONSOLE_SCREEN_BUFFER_INFO sbi;
 if (!GetConsoleScreenBufferInfo(hScreen, &sbi)) return;
 TCHAR szBuf[80];
 StringCchPrintf(szBuf, 80, TEXT("Pos = %3d, %3d"),
                 sbi.dwCursorPosition.X,
                 sbi.dwCursorPosition.Y);
 DWORD dwWritten;
 COORD coDest = { 0, sbi.srWindow.Bottom };
 WriteConsoleOutputCharacter(hScreen, szBuf, lstrlen(szBuf),
    coDest, &dwWritten);
}

Our lame-o status bar consists of the current cursor position. Notice that the console subsystem does not follow the GDI convention of endpoint-exclusive rectangles.

int __cdecl wmain(int argc, WCHAR **argv)
{
 HANDLE hConin = CreateFile(TEXT("CONIN$"),
                            GENERIC_READ | GENERIC_WRITE,
                            FILE_SHARE_READ | FILE_SHARE_WRITE,
                            NULL, OPEN_EXISTING, 0, NULL);
 if (hConin == INVALID_HANDLE_VALUE) return 1;

 HANDLE hConout = CreateFile(TEXT("CONOUT$"),
                       GENERIC_READ | GENERIC_WRITE,
                       FILE_SHARE_READ | FILE_SHARE_WRITE,
                       NULL, OPEN_EXISTING, 0, NULL);
 if (hConout == INVALID_HANDLE_VALUE) return 1;

We start by getting the handles to the current console. Since we are a fullscreen program, we don't rely on stdin and stdout. (How do you position the cursor on a redirected output stream?)

 HANDLE hScreen = CreateConsoleScreenBuffer(
                       GENERIC_READ | GENERIC_WRITE,
                       0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
 if (!hScreen) return 1;

 SetConsoleActiveScreenBuffer(hScreen);

We create a new screen buffer and switch to it, so that our work doesn't disturb what was previously on the screen.

 DWORD dwInMode;
 GetConsoleMode(hConin, &dwInMode);

We start by retrieving the original console input mode before we start fiddling with it, so we can restore the mode when our program is finished.

 SetConsoleCtrlHandler(NULL, TRUE);
 SetConsoleMode(hConin, ENABLE_MOUSE_INPUT |
                        ENABLE_EXTENDED_FLAGS);

We set our console control handler to NULL (which means "don't terminate on Ctrl+C") and enable mouse input on the console because we're going to be tracking the mouse position in our status bar.

 CONSOLE_SCREEN_BUFFER_INFO sbi;
 if (!GetConsoleScreenBufferInfo(hConout, &sbi)) return 1;

 COORD coDest = { 0, sbi.srWindow.Bottom - sbi.srWindow.Top };
 DWORD dw;
 FillConsoleOutputAttribute(hScreen,
     BACKGROUND_BLUE |
     FOREGROUND_BLUE | FOREGROUND_RED |
     FOREGROUND_GREEN | FOREGROUND_INTENSITY,
     sbi.srWindow.Right - sbi.srWindow.Left + 1,
     coDest, &dw);

We retrieve the screen buffer dimensions and draw a blue status bar at the bottom of the screen. Notice that the endpoint-inclusive rectangles employed by the console subsystem result in what look like off-by-one errors. The bottom line of the screen is Bottom - Top, which in an endpoint-exclusive world would be the height of the screen, but since the rectangle is endpoint-inclusive, this is actually the height of the screen minus 1, which puts us at the bottom line of the screen. Similarly Right - Left is the width of the screen minus 1, so we have to add one back to get the width.

 DrawStatusBar(hScreen);

Draw our initial status bar.

 INPUT_RECORD ir;
 BOOL fContinue = TRUE;
 while (fContinue && ReadConsoleInput(hConin, &ir, 1, &dw)) {
  switch (ir.EventType) {
  case MOUSE_EVENT:
   if (ir.Event.MouseEvent.dwEventFlags & MOUSE_MOVED) {
     SetConsoleCursorPosition(hScreen,
         ir.Event.MouseEvent.dwMousePosition);
     DrawStatusBar(hScreen);
   }
   break;
  case KEY_EVENT:
   if (ir.Event.KeyEvent.wVirtualKeyCode == VK_ESCAPE) {
    fContinue = FALSE;
   }
   break;
  }
 }

This is the console version of a "message loop": We read input events from the console and respond to them. If the mouse moves, we move the cursor to the mouse position and update the status bar. If the user hits the Escape key, we exit the program.

 SetConsoleMode(hConin, dwInMode);
 SetConsoleActiveScreenBuffer(hConout);
 return 0;
}

And when the program ends, we clean up: Restore the original input mode and restore the original screen buffer.

If you run this program, you'll see a happy little status bar at the bottom whose contents continuously reflect the cursor position, which you can move by just waving the mouse around.

If you want a status bar in your console program, go ahead and draw it yourself. Of course, since it's a console program, your status bar is going to look console-y since all you have to work with are rectangular character cells. Maybe you can make use of those fancy line-drawing characters. Party like it's 1989!

Comments (30)
  1. Anonymous says:

    "How do you position the cursor on a redirected output stream?"

    ANSI.SYS!  You'll have to compile as 16-bit though.  (However I haven't tried this since NT4, so I don't know if it still works.)

  2. Anonymous says:

    The only time I remember seeing console windows have a command-line is in a UNIX-like environment that is running the screen console multiplexer.

    It makes sense in that context, as it allows you to have multiple shells running simultaneously, and the status bar was used to list the names for each console (screen lets you name the consoles it starts) so you could tell which console was at which console number.

  3. Anonymous says:

    The only time I remember seeing console windows have a status-line is in a UNIX-like environment that is running the screen console multiplexer.

    It makes sense in that context, as it allows you to have multiple shells running simultaneously, and the status bar was used to list the names for each console (screen lets you name the consoles it starts) so you could tell which console was at which console number.

  4. nathan_works says:

    There's also the CMD title function as well.. Who says that can't be used for status as well ? (Since you'd see the title in the task bar …)

  5. Anonymous says:

    s/progarm/program/ (then feel free to delete this comment)

  6. Anonymous says:

    I think you went a bit too far…

    But while on the topic of console applications, I just want to say that MS has made the only text mode editor worth using. Screw emacs and vi, edit.com is the best one.

  7. Anonymous says:

    I'm going to make a wild guess and suggest that what the person really wanted was to add a real windows-y status bar to his console. And as long as we're on the subject – can you add menus and other controls too? How about hosting a console window as a control within your own app?

  8. Dan Bugglin says:

    Vilx: Thanks to redirecting stdin and stdout, you can host a bunch of apps in any window you want.

    Of course the caveat is that stdout will only deliver output in complete lines; the command prompt will not be streamed until the user types a command!  Catch 22.

    There is a DLL used in the Console2 project (google it, it's on sourceforge) that can hide and read the full console contents of console windows and mirror them elsewhere.  Not sure how it works though.

    The CONIN$ and CONOUT$ special files mentioned in this post might also be useful for the same purpose.  However the aforementioned DLL works with any console program while I assume CONIN$ and CONOUT$ are only useful for the current program.

    I'm guessing DLL injection would have to come into play for whatever solution you choose…

  9. Anonymous says:

    @Dan: Weird, cygwin ssh can host cmd.exe as a child process on pseudo-tty just fine (cygwin psuedo ttys are implemented as UNIX domain sockets, which are in turn implemented as TCP/IP sockets connected to localhost).

  10. Anonymous says:

    Thanks for answering my question! And yes, I add a character “informational line” to the bottom of my console apps. Yes the console API is a different philosophy than a Windows message pump, but regardless provides tons of flexibility for such apps.

    However, I feel like a foolish heckler whom now the spotlight has moved to. Like the comments above, I think that indeed I was thinking of a “real” windows-y status bar, that we could manipulate just like the TITLE command to change the title.

    Of course, what seems like solid gold at the time fades into a mild amusement when looked at through the beer goggles of time.

  11. Anonymous says:

    @Vlix: not unless you're willing to reimplement the frame. csrss won't let you screw around with its memory space (can you say privilege escalation attack? Yes you can).

  12. Anonymous says:

    @Dan Bugglin: Actually, Console2 does exactly what you say — it injects a DLL into the running process, and then polls CONIN$ and CONOUT$ at specified intervals.

    As you might imagine, this doesn't work when a program allocates more than one console.  Fortunately, most programs don't do this.  But any program that does it will hang Console2.  As far as I'm aware, there isn't a 100% reliable way to write a terminal app for Windows console-mode applications.

    @Joshua: See "Using Cygwin effectively with Windows" in the Cygwin documentation for a discussion of the problems that Cygwin encounters.  cmd.exe works because it pipes gracefully.  Any program that'll hang Console2 will likely also cause problems with Cygwin.

  13. Anonymous says:

    @Joshua: Unfortunately cmd.exe doesn't work terribly well in Cygwin pty-based terminals, because it relies on the console window for its command line editing and history facilities, whereas Unix shells such as bash do that themselves. Also, Cygwin's ptys are not based on sockets, but on Windows pipes. Hence native Windows programs get to see pipes where they expect a console when running in a terminal based on Cygwin ptys. That often causes problems.

    On a related note: Raymond, any chance of the protocol between csrss.exe and Windows 7's conhost.exe being published, to allow people to write replacements for conhost.exe with a nicer UI than the default? Console2 has a good go at the latter, but has to employ somewhat imperfect trickery with a hidden console window.

  14. Anonymous says:

    @Tom: I don't think Console2 does any DLL injection. It creates a hidden console window and uses WriteConsoleInput() to put keyboard and mouse events into the console's input buffer and ReadConsoleOutput() to get at its screen buffer. One problem is that there's no notification of screen buffer changes, so Console2 has to poll it regularly.

  15. Anonymous says:

    When will the Suggestion Box open again ?

    We need more microsoft employees blogging and answering and helping to answer questions :)

  16. Anonymous says:

    I do not know how it does, but the .Net console library handles these tasks very well. It can get/set cursor position, change colors, set window size, and so on.

    And it does it in a very intuitive way. For example it you want a square dimensioned console:

    System.Console.WindowHeight = System.Console.WindowWidth;

    Maybe it's time to disassemble and learn some more stuff.

  17. Anonymous says:

    I've actually wondered whether msdn.microsoft.com/…/ms971319.aspx could be used instead of polling the console.

  18. Anonymous says:

    It seems clear that many people do not realize that it is CSRSS.EXE which automatically creates console windows not CMD.EXE or powershell.exe, et al. Windows detects that an application needs a console window to host it and creates one.

    Since CSRSS.EXE runs in the security context of SYSTEM normal users shouldn't have access to mess with them. They have to be protected which means that you can't be allowed to inject code into the CSRSS.EXE process to do stuff like attach arbitrary menus and whatnot to their chrome. Note that this is why they aren't themed in Widnows XP and why the scrollbar looks old school in Vista.

    Starting with Windows 7, CSRSS.EXE creates conhost.exe processes to host the console window in the same security context as the program it is hosting which removes the possibility of an escalation of privilege through the console window.

    @Andy brings up an interesting possibility that since conhost.exe is decoupled from CSRSS.EXE, it could potentially be replaced with something that resized its buffers on resize and had a line-oriented copy. That would be very cool.

  19. Yuhong Bao says:

    "Raymond, any chance of the protocol between kernel32.dll and Windows 7's conhost.exe being published, to allow people to write replacements for conhost.exe with a nicer UI than the default?"

    Not to mention handy for emulating terminals with a telnet or ssh server.

    BTW, FTFY from cmd.exe to kernel32.dll which is what actually do the IPC to conhost.exe.

  20. Yuhong Bao says:

    OS/2 had DETACH that allow running background programs that could pop up on top of the current session using hotkeys. Originated in Multitasking DOS 4.0 that ended up never being released outside I think France.

  21. Anonymous says:

    This post has another good use as well – how to write fancy Windows conaole programs. I always wondered how it was done. Thanks, Raymond!

  22. Anonymous says:

    It's nice to see how it's done properly!

    I have written a couple of "fancy Windows console programs" myself, they work fine but are very amateurish so I won't post the link ;).

  23. Anonymous says:

    @Back door: Good point. I haven't looked into the Console Accessibility API.

    @yuhong2: I didn't say 'cmd.exe' but 'csrss.exe'.

  24. Anonymous says:

    I've not used Console2 but I was asked to rewrite a batch file not to use start because apparently Console2 isn't aware of the new window. (I wanted to use start because I didn't want the "Terminate batch job?" message if you hit Ctrl+C. Is there another way around that?)

    I've always wanted a good example of how to use ReadConsoleInput, in particular waiting for someone to press any key. (But I also want to be able to read by lines before and afterwards.)

  25. Anonymous says:

    Interesting. I didn't know about the csrss.exe and conhost.exe. I wonder – what were the reasons for a convoluted solution like this? Why not simply spawn another thread that would handle the console window in the same process?

  26. Anonymous says:

    "Why not simply spawn another thread that would handle the console window in the same process?"

    Because multiple processes can share the same console, and the console only disappears once all the processes sharing it have exited.

  27. Anonymous says:

    Whoops, I only meant to comment once earlier… the transmission didn't appear to send before my network connection died, and before I submitted again, I fixed a braino.

  28. Anonymous says:

    @Reiter: "since conhost.exe is decoupled from CSRSS.EXE, it could potentially be replaced with something that resized its buffers on resize and had a line-oriented copy. That would be very cool."

    Welcome… to the Windows of the FUTURE! Or, you know, the Unix of the mid-80s.

    I'm by no means a Linux fanboy (I can rant about various dumb things it does for a long time too), but I don't understand how the Windows terminal window is still as terrible as it is. I had some hope when Powershell came out that they might revamp that too, but no, you still have to use it with that god-awful UI.

  29. Anonymous says:

    Interesting – I would have taken the question as referring to the way you could put escape sequences in your PROMPT which would display information at the top or bottom of your screen. The Windows 'title' command could be used in a similar way, presumably, except you wouldn't get automatic updates of things like the current working directory that way.

  30. Anonymous says:

    @Back door: the accessibility API has its own set of issues when dealing with the console. Because the console events are initiated from csrss.exe (i.e., not the application process that is writing to the console) the events are received asynchronously. This means that the contents of the console could have changed again before the event can be processed (which may or not be a problem, depending on what you're trying to do with it). Or at least that's the way it is on XP.

Comments are closed.