13

I'm trying to write a program in which a number starts from 0, but when you press any key, it gets incremented by 1. If nothing is pressed, it keeps on decreasing by 1 per second until it reaches 0. Every increment or decrement is displayed on the console window.

Problem with my approach is that nothing happens until I press a key (that is, it checks if anything is pressed with getch()). How do I check that nothing is pressed? And of course, !getch() doesn't work because for that to work, it'll still need to check for keypress which nullifies the purpose itself.

OS: Windows 10 Enterprise, IDE: Code::Blocks

void main()
{
    int i, counter = 0;
    for (i = 0; i < 1000; i++)
    {
        delay(1000);
        // if a key is pressed, increment it
        if (getch())
        {
            counter += 1;
            printf("\n%d", counter);
        }
        while (counter >= 1)
        {
            if (getch())
            {
                break;
            }
            else
            {
                delay(1000);
                counter--;
                printf("\n%d", counter);
            }
        }
    }
}
Ritika
  • 149
  • 1
  • 5
  • 7
    Why not? If there's some adverse effect I don't know about, what alternative can I use to simulate a delay? Nested for loops are highly CPU consuming so I avoided that. I do not want the decrement to happen immediately because that woudn't give the user enough time to increment it in the first place. – Ritika Oct 09 '18 at 07:54
  • If you want to do multiple things at once (waiting for delay to finish and waiting for a keystroke), you might want to look into [multithreading](https://www.geeksforgeeks.org/multithreading-c-2/) – MemAllox Oct 09 '18 at 07:58
  • How would you handle a keypress while you are blocked in a `delay()`? – Gerhardh Oct 09 '18 at 08:26
  • For Windows you want https://stackoverflow.com/questions/41212646/get-key-press-in-windows-console : call GetNumberOfConsoleInputEvents and if it's above zero call ReadConsoleInput. – pjc50 Oct 09 '18 at 15:28
  • If this is for a real-life thing, consider using a third-party event library, like https://nikhilm.github.io/uvbook/basics.html. You'd have two listeners, a timer and a stream, and each handler would increment/decrement. – Peter Oct 09 '18 at 16:31
  • "Nested for loops are highly CPU consuming". That is not a generally true statement. – Mad Physicist Oct 10 '18 at 15:59

8 Answers8

11

The following short program requires neither ncurses or threads. It does, however, require changing the terminal attributes - using tcsetattr(). This will work on Linux and Unix-like systems, but not on Windows - which does not include the termios.h header file. (Perhaps see this post if you are interested in that subject.)

#include <stdio.h>
#include <string.h>
#include <termios.h>

int main(int argc, char *argv[]) {
    struct termios orig_attr, new_attr;
    int c = '\0';
    // or int n = atoi(argv[1]);
    int n = 5;

    tcgetattr(fileno(stdin), &orig_attr);
    memcpy(&new_attr, &orig_attr, sizeof(new_attr));
    new_attr.c_lflag &= ~(ICANON | ECHO);
    new_attr.c_cc[VMIN] = 0;
    // Wait up to 10 deciseconds (i.e. 1 second)
    new_attr.c_cc[VTIME] = 10; 
    tcsetattr(fileno(stdin), TCSANOW, &new_attr);

    printf("Starting with n = %d\n", n);
    do {
        c = getchar();
        if (c != EOF) {
            n++;
            printf("Key pressed!\n");
            printf("n++ => %d\n", n);
        } else {
            n--;
            printf("n-- => %d\n", n);
            if (n == 0) {
                printf("Exiting ...\n");
                break;
            }
            if (feof(stdin)) {
                //puts("\t(clearing terminal error)");
                clearerr(stdin);
            }
        }
    } while (c != 'q');

    tcsetattr(fileno(stdin), TCSANOW, &orig_attr);

    return 0;
}

The vital points are that

new_attr.c_lflag &= ~(ICANON | ECHO);

takes the terminal out of canonical mode (and disables character 'echo'),

new_attr.c_cc[VMIN] = 0;

places it in polling (rather than 'blocking') mode, and

new_attr.c_cc[VTIME] = 10;

instructs the program to wait up till 10 deciseconds for input.

Update (2019-01-13)

  • add clearerr(stdin) to clear EOF on stdin (seems to be necessary on some platforms)
David Collins
  • 2,852
  • 9
  • 13
  • `int c` to be able to safely compare with `EOF` :) – pmg Oct 09 '18 at 09:56
  • @pmg: Thanks. I see that ilkkachu has already made the relevant edit. – David Collins Oct 09 '18 at 10:16
  • An alternative to setting `VTIME` would be to use `select()` to see if input becomes available within a particular time period. You'd still need to put the terminal in raw mode to be able to detect anything but full lines, though. – ilkkachu Oct 09 '18 at 10:25
  • Doesn't work for me (FreeBSD 11.2) :( Basically counting up is ok. but the first count down (if I don't hit a key within a second) goes all the way to `0` in a flash. – pmg Oct 09 '18 at 12:21
  • @pmg I witnessed a similar problem on certain Android platforms. Clearing the `EOF` flag on `stdin` solved the problem. (I have updated the answer.) I would be curious if that fixes the problem for you on FreeBSD also. – David Collins Jan 13 '19 at 11:40
  • @DavidCollins: I'll check tomorrow, can't reach my FreeBSD box today. – pmg Jan 13 '19 at 12:40
  • @pmg Cool. I look forward to learning how it goes - if / when you have time. – David Collins Jan 13 '19 at 13:34
  • David: with your new edit, it works on FreeBSD! I also tested on a Debian 9 (stretch) on which it works with or without the `clearerr()` call. However, for the debian box, I needed to `#define _POSIX_C_SOURCE 200112L` to obtain a clean compilation. – pmg Jan 14 '19 at 09:20
4

This could be done with multithreading as already suggested, but there are other possibilities.

ncurses for example has the possibility to wait for input with a timeout.

An example for ncurses (written by Constantin):

initscr();
timeout(1000);
char c = getch();
endwin();
printf("Char: %c\n", c);

I think poll could also be used on stdin to check if input is available.

And to make your program more responsive you could lower your sleep or delay to for example 100ms and only decrement if ten iterations of sleep have passed without input. This will reduce the input lag.

Kami Kaze
  • 2,069
  • 15
  • 27
2

Here is a pthread example that works on linux. The concept is ok, but there are probably existing loops/libraries for this.

#include <stdio.h>
#include<pthread.h>


void *timer(void* arg){
    int* counter = (int*)arg;
    while(*counter > 0){
        int a = *counter;
        printf("counter: %d \n", a);
        *counter = a - 1;
        sleep(1);
    }
}

int main(int arg_c, char** args){
    int i = 100;
    pthread_t loop;

    pthread_create(&loop, NULL, timer, &i);

    while(i>0){
        i++;
        getchar();
        printf("inc counter: %d \n", i);
    }
    printf("%d after\n", i);

    pthread_join(loop, NULL);

    return 0;
}

This starts a second thread, which has the countdown on it. That decrements the counter every second. On the main thread it has a loop with getchar. They both modify i.

matt
  • 10,892
  • 3
  • 22
  • 34
2

You need use thread, and need use __sync_add_and_fetch and __sync_sub_and_fetch to avoid concurrency problem

#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <termios.h>
#include <unistd.h>
#include <iostream>

static void* thread(void* p) {
    int* counter = (int*)p;
    while (1) {
        if (*counter > 0) {
            __sync_sub_and_fetch(counter, 1);
            printf("sub => %d\n", *counter);
        }  else {
            sleep(1);
        }
    }

    return NULL;
}

int main() {
    int counter = 0;
    char ch;

    struct termios orig_attr, new_attr;
    tcgetattr(fileno(stdin), &orig_attr);
    memcpy(&new_attr, &orig_attr, sizeof(new_attr));
    new_attr.c_lflag &= ~(ICANON | ECHO);
    tcsetattr(fileno(stdin), TCSANOW, &new_attr);

    pthread_t pid;
    if (pthread_create(&pid, NULL, thread, &counter)) {
        fprintf(stderr, "Create thread failed");
        exit(1);
    }

    while(1) {
      char c = getchar();
      __sync_add_and_fetch(&counter, 1);
      printf("add: %d\n", counter);
    }

    return 0;
}
sundb
  • 490
  • 2
  • 8
2

Another example using ncurses and POSIX timers and signals (and global variables).

#include <ncurses.h>
#include <signal.h>
#include <time.h>

int changed, value;

void timer(union sigval t) {
        (void)t; // suppress unused warning
        changed = 1;
        value--;
}

int main(void) {
        int ch;
        timer_t tid;
        struct itimerspec its = {0};
        struct sigevent se = {0};

        se.sigev_notify = SIGEV_THREAD;
        se.sigev_notify_function = timer;
        its.it_value.tv_sec = its.it_interval.tv_sec = 1;
        timer_create(CLOCK_REALTIME, &se, &tid);
        timer_settime(tid, 0, &its, NULL);

        initscr();
        halfdelay(1); // hit Ctrl-C to exit
        noecho();
        curs_set(0);

        for (;;) {
                ch = getch();
                if (ch != ERR) {
                        changed = 1;
                        value++;
                }
                if (changed) {
                        changed = 0;
                        mvprintw(0, 0, "%d ", value);
                        refresh();
                }
        }

        endwin();
}
pmg
  • 106,608
  • 13
  • 126
  • 198
2

Here is another way that uses select to check if input exists and also to wait. Not a pretty solution but it works. Linux only though.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdbool.h>
#include <sys/select.h>

#define WAIT_TIME 1000 //Delay time in milliseconds

bool inputExists(void)
{
    fd_set readfds;
    FD_ZERO(&readfds);
    FD_SET(0, &readfds);

    struct timeval tv;
    tv.tv_sec = tv.tv_usec = 0;

    if(select(1, &readfds, NULL, NULL, &tv))
        return true;
    else
        return false;
}

void wait()
{
    struct timeval tv;
    tv.tv_sec = 0;
    tv.tv_usec = WAIT_TIME * 1000;
    select(0, NULL, NULL, NULL, &tv);
}

int main(void)
{
    system("stty raw"); /* Switch to terminal raw input mode */

    unsigned int count = 0;
    for(;;)
    {
        if(inputExists())
        {
            char input[256] = {0};
            read(0, input, 255);
            count += strlen(input);

            printf("\rCount is now %d\n", count);
        }
        else if(count > 0)
        {
            count--;
            printf("\rDecremented count to %d\n", count);
        }

        puts("\rWaiting...");
        wait();
    }
}

A better way that avoids system("stty raw") and those \rs would be to use tcgetattr and tcsetattr:

struct termios orig_attr, new_attr;

tcgetattr(STDIN_FILENO, &orig_attr);
new_attr = orig_attr;
new_attr.c_lflag &= ~(ICANON | ECHO); //Disables echoing and canonical mode
tcsetattr(STDIN_FILENO, TCSANOW, &new_attr);

//...

tcsetattr(STDIN_FILENO, TCSANOW, &old_attr);
Spikatrix
  • 20,225
  • 7
  • 37
  • 83
1

If you're not bothered about portability, and you'll always be using Windows, you can use PeekConsoleInput, which tells you what console input events are waiting.

You can't (easily) use ReadConsoleInput, because it blocks until there is at least one pending input event.

Roger Lipscombe
  • 89,048
  • 55
  • 235
  • 380
0

Your code has two problems; one serious, one not.

The first problem, as you have found out, is that getch() is a blocking function. In other words, the function call will not return until a key had been pressed.

The second problem, although minor, is that the program only responds to input every second.

I've modified your requirements slightly, starting the initial counter at 5.

#include <windows.h>

int main(void)
{
  int Counter;
  time_t StartTime;
  DWORD EventCount;


  Counter=5;
  do
  {
    StartTime=time(NULL);
    do
    {
      Sleep(10);  /* in ms. Don't hog the CPU(s). */
      GetNumberOfConsoleInputEvents(GetStdHandle(STD_INPUT_HANDLE),&EventCount);
    }
    while( (StartTime==time(NULL)) && (EventCount==0) );  
        /* Wait for a timeout or a key press. */

    if (EventCount!=0)  /* Have a key press. */
    {
      FlushConsoleInputBuffer(GetStdHandle(STD_INPUT_HANDLE));  /* Clear the key press. */
      Counter++;
    }
    else  /* Timed out. */
      Counter--;

    printf("Counter = %d\n",Counter);
  }
  while(Counter>0);

  return(0);
}

Compiled using Microsoft Visual C++ 2015 (command line: "cl main.c").
Tested on Windows 7 and 10.

A.Wise
  • 1
  • 1