5

I would like to do the following in a command-line application on OS X:

while (true) {
    // do some task
    if (user_has_pressed('x')) {
        // break out of the loop and do something different
    }
}

For clarity, I don't want the program to block waiting for user input. I'm running a numerical simulation that takes many hours to run, and I want to press a key to print detailed statistics on its progress, or interrupt it and change parameters, etc.

There are some existing similar questions, but the answers either suggest a windows-only getch function, or they switch the terminal into a different input mode. I don't want to do that, because I need to retain the ability to interrupt with ctrl-c without messing up the terminal.

I don't want to build a Cocoa application, and I don't care about being cross-platform. I'm just looking for the simplest quick-and-dirty way to do it in a command line app that will only ever be run on my own machine.

I guess one option is to use ncurses. From a brief bit of reading it seems like a heavier option than I'd like - but if somebody would post a simple minimal example that accomplishes the above task that would be really helpful.

Community
  • 1
  • 1
N. Virgo
  • 7,970
  • 11
  • 44
  • 65

8 Answers8

4

Are you looking for the following behavior?

   #include <pthread.h>
   #include <iostream>

   static volatile bool keep_running = true;

   static void* userInput_thread(void*)
   {
       while(keep_running) {
           if (std::cin.get() == 'q')
           {
               //! desired user input 'q' received
               keep_running = false;
           }
       }
   }

   int main()
   {
      pthread_t tId;
      (void) pthread_create(&tId, 0, userInput_thread, 0);

      while ( keep_running )
      {
         //! this will run until you press 'q'
      }

      (void) pthread_join(tId, NULL);

      return 0;
   }
Bharath
  • 111
  • 9
  • Yes - aside from the little nitpick that one has to press 'q' and then 'enter' rather than just 'q', this does do what I want. Since it is both simple and works I will accept it unless another option comes along that avoids the need to press 'enter' – N. Virgo Dec 08 '16 at 07:22
  • you can try the below version of "userInput_thread, for making it work without enter key (http://stackoverflow.com/questions/421860/capture-characters-from-standard-input-without-waiting-for-enter-to-be-pressed). – Bharath Dec 08 '16 at 07:56
  • static void* userInput_thread(void*) { system("stty raw"); // Set terminal to raw mode while(true) { if (getchar() == 'q') { //! desired user input 'q' received keep_running = false; system("stty cooked"); // Reset terminal to normal "cooked" mode } } } – Bharath Dec 08 '16 at 08:01
  • I'm awarding this answer the bounty, as it was the first one that worked for me. – N. Virgo Dec 12 '16 at 05:40
1

The basic idea is to use poll, but the terminal would normally wait for an enter key before it sends the key press to stdin. So we need disable the canonial mode first.

#include <poll.h>
#include <sys/types.h>
#include <sys/uio.h>
#include <unistd.h>
#include <string.h>
#include <termios.h>
#include <iostream>

void disable_canonical(){
    struct termios old = {0};
    if (tcgetattr(0, &old) < 0)
        perror("tcsetattr()");
    old.c_lflag &= ~ICANON;
    old.c_lflag &= ~ECHO;
    old.c_cc[VMIN] = 1;
    old.c_cc[VTIME] = 0;
    if (tcsetattr(0, TCSANOW, &old) < 0)
        perror("tcsetattr ICANON");
}
void enable_canonical(){
    struct termios old = {0};
    if (tcgetattr(0, &old) < 0)
        perror("tcsetattr()");
    old.c_lflag |= ICANON;
    old.c_lflag |= ECHO;
    old.c_cc[VMIN] = 1;
    old.c_cc[VTIME] = 0;
    if (tcsetattr(0, TCSANOW, &old) < 0)
        perror("tcsetattr ICANON");
}
bool key_pressed(char c){
    struct pollfd fds[1];
    fds[0].fd = 0;
    fds[0].events = POLLIN;
    fds[0].revents = 0;
    int r = poll(fds, 1, 1);
    if(r > 0){
        if(fds[0].revents & POLLIN || fds[0].revents & POLLRDBAND || fds[0].revents & POLLRDNORM){
            char buffer[1024];
            memset(buffer, 0, sizeof(buffer));
            int n = read(0, buffer, sizeof(buffer) -1);
            for(int i = 0; i < n; ++i){
                if(buffer[i] == c){
                    return true;
                }
            }
        }
    }
    return false;
}

int main(){
    disable_canonical();
    while(true){
        if(key_pressed('x')){
            break;
        }
        usleep(500);
        //std::cout << "looping...\n";
    }
    enable_canonical();
}

P.S. the tcsetattr code is borrowed from https://stackoverflow.com/a/912796/573789

Community
  • 1
  • 1
qianfg
  • 878
  • 5
  • 8
  • I can't pretend to understand what's happening here, but this one does seem to work as expected. – N. Virgo Dec 12 '16 at 05:39
  • poll is used to check whether there is something to be read from stdin(0). And disable_canonical is used to make sure that whenever you pressed anything on the keyborad, the tty will send it immediately to stdin, normally it would cache the key press and send it to stdin only when Enter is pressed.. – qianfg Dec 12 '16 at 08:30
1

I believe what you are looking for is how to break out of the loop but keep the program still running.

One of the solution for your problem can be the use of the goto C++ keyword. Please look at the following code:

while(true)
{
   char pressKeyToQuit = 'q';
   cin >> pressKeyToQuit;

   if(pressKeyToQuit == 'q')
   {
       goto getOffTheLoop; //to do something different
   }

   getOffTheLoop:
 //do something different
  cout << "Processing!\n";

}

Hope I helped! :D

Samir
  • 47
  • 1
  • 10
0

The following can help:

#include <iostream>
using namespace std;

int main() {
    while (cin.get() != 'x') {
        cout << "I am running." << endl;
    }
    return 0;
}

But if you don't want your program to stop, use a thread as mentioned in this answer.

Community
  • 1
  • 1
Praveen Vinny
  • 2,372
  • 6
  • 32
  • 40
  • This will not work, since it requires a character to be input (and then return to be pressed) on every iteration of the loop. I want the loop to keep running without user interaction, until I decide to interrupt it. – N. Virgo Dec 04 '16 at 13:22
0

You can use the POSIX signals:

#include <signal.h>

static volatile bool keep_continue = true;

int main()
{
    signal(SIGINT, [](int) { keep_continue = false; });

    while (keep_continue)
    {
        // run until you press ctrl-c
    }
}
Boiethios
  • 38,438
  • 19
  • 134
  • 183
  • This looks like the "right" approach, but when I run it within XCode, pressing CMD-. just kills it (it doesn't reach code after the while loop), whereas prssing ctrl-C does nothing. (I'm not sure of exactly what the difference is between those two keypresses in OS X, as they often seem to be synonymous.) – N. Virgo Dec 08 '16 at 03:09
  • 1
    Hmm, it seems from some other questions that CMD-. sends SIGKILL in XCode, and there seems to be no convenient way to send SIGINT. That's quite annoying, as this would be a very convenient solution for me otherwise. – N. Virgo Dec 08 '16 at 03:14
0

As a hack( dirty but fast ) solution consider a file that interrupts execution of the loop...

#include <fstream>
#include <iostream>
#include <unistd.h>
int main()
{
    while( 1 )
    {

        std::cout << "start" << std::endl;

        std::ifstream ifs( "stop" );

        if( ifs.good() )
        {
            std::cout << "exit" << std::endl;
            ifs.close();
            break;
        }
        ifs.close();
        ifs.open( "pause" );
        while( ifs.good() )
        {
            usleep( 100 );
            ifs.close();
            ifs.open( "pause" );
        }

        std::cout << "end" << std::endl;
    }
}

For the given program above, to pause it, create a pause file in the same folder as your executable

touch pause

or

> pause (in mac os)

To unpause,

rm pause

To stop,

> stop (don't forget to rm stop before you run executable again)

Use aliases to reduce number of chars to type

alias p='touch ..path_to_executable../pause'

alias s='touch ..path_to_executable../stop'

alias run='if [ -f "..path_to_executable../pause"] ; then rm "..path_to_executable../pause" fi;if [ -f "..path_to_executable../stop"] ; then rm "..path_to_executable../stop" fi; ..path_to_executable../..executable..'

pnoob
  • 19
  • 2
0

Have you tried the non-blocking read?

#include <unistd.h>
#include <fcntl.h>

int main()
{
    int flags = fcntl(0, F_GETFL, 0); // we get the current flags applying on input file descriptor
    fcntl(0, F_SETFL, flags | O_NONBLOCK); // add the non-blocking flag to input file descriptor

    while (read(0, NULL, 0u) < 0)
    {
        // run until you press ENTER key
    }
}

The program exits when you press ENTER. It works on Debian 9, but I have no OS X within easy reach to test on it.

Boiethios
  • 38,438
  • 19
  • 134
  • 183
  • It doesn't work for me, at least when running in XCode. The call to `read` always returns 0, regardless of any keys that are pressed. – N. Virgo Dec 11 '16 at 13:28
0

I think you should see the article on - http://cc.byexamples.com/2007/04/08/non-blocking-user-input-in-loop-without-ncurses/

I would have commented this, but I don't have enough reputation.

JedaiCoder
  • 646
  • 7
  • 12