├── .gitignore ├── LICENSE ├── README.md ├── examples ├── BouncingO.c ├── helloWorld.c └── helloWorldNew.c └── images ├── hello1.png ├── hello3.png └── hello4.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | \.DS_Store 55 | 56 | examples/bounce 57 | 58 | examples/hello 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Paul Harrington 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Guide to making your first command line project with ncurses 2 | 3 | ## Intro 4 | Everyone's first programming experience is on some form of command line, usually a hello world in your language of choice. 5 | 6 | ``` 7 | int main() { 8 | printf("Hello World!\n"); 9 | return EXIT_SUCCESS; 10 | } 11 | ``` 12 | 13 | But eventually you grow out of it, you start to think "the command line is only for debugging" well I'm here to defend the command line, and all of its glory. Writing full-blown apps in the command line is not only fun, but its easy, and it can look damn good (assuming you're down for a retro look). Making a game or app as a command line app is quicker and easier than messing around with complex graphics libraries, or god forbid, css. 14 | 15 | This guide is going to be a look at how you can make command line apps and games (like [this one](https://github.com/harrinp/WalledIn)) simply and easily using the ncurses library. ncurses is a very common library for posix-compliant (_typically this means Macos and Linux_) command lines. 16 | 17 | ##### Caveats 18 | 19 | This guide isn't going to go over anything to do with windows, ncurses supports posix compliant terminals. You may be able to get ncurses working on windows, but I offer no guarantees. Further, there are other libraries like ncurses which are for windows, namely [conio.h](https://en.wikipedia.org/wiki/Conio.h). I won't cover it here but the principles should be similar. 20 | 21 | ###### Other guides 22 | I'm not the expert, just a dev who already went through this learning process. This guide is going to be a bare-bones "get up and running" lesson, you can find a more detailed explanation of ncurses and all of its capabilities [here](https://invisible-island.net/ncurses/ncurses.faq.html) and [here](http://tldp.org/HOWTO/NCURSES-Programming-HOWTO/). 23 | 24 | ###### Language choice 25 | ncurses, like many libraries is a C library, I wrote all of these programs and examples in C, partly because that's the library language, and partly because C is beautiful. I'm a firm believer that programmers should have at least a working knowledge of the C language since it's a building block of basically everything. But I digress, you could also use C++ or something else, but you might need a different library or a connection method, my knowledge is nonexistant in this area. I recommend trying these examples in C, even if you aren't a C guru these examples should be easy to understand and expand upon. Have fun! 26 | 27 | ##### Example files 28 | 29 | All example files will be included in the examples folder of this repository. 30 | 31 | ## Getting started 32 | 33 | ### Hello world v2 34 | 35 | Lets redo our old hello world program and give it a fancy twist. 36 | 37 | Make sure you include ncurses.h 38 | ``` 39 | #include 40 | ``` 41 | 42 | Now we need to initialize ncurses, and make sure we leave ncurses mode when we're done. This is done with initscr() and endwin(). 43 | ``` 44 | int main() { 45 | initscr(); 46 | printf("Hello World!\n"); 47 | endwin(); 48 | return EXIT_SUCCESS; 49 | } 50 | ``` 51 | 52 | Now we need to update our printf with the ncurses variant, printw(). This works the same way as printf. Then we need to refresh() so that the data we've put in is shown. Then theres getch(), getch() is the ncurses function to get input as a character, but here we're using it so that the program will wait for a character before endwin(), where it clears the screen and returns to normal mode. 53 | 54 | ``` 55 | int main() { 56 | initscr(); 57 | printw("Hello World!"); 58 | refresh(); 59 | getch(); 60 | endwin(); 61 | return EXIT_SUCCESS; 62 | } 63 | ``` 64 | Compile - make sure to link ncurses with -lncurses 65 | ``` 66 | gcc helloWorld.c -o hello -lncurses 67 | ``` 68 | And run with `./hello`! 69 | 70 | ![Not super exciting](images/hello3.png) 71 | 72 | Fantastic, but thats not interesting. Lets spice it up. I'm going to now use mvprintw(), its the same as printw but you need to give it two coordinates, y then x, as the first two parameters. 73 | ``` 74 | int main() { 75 | initscr(); 76 | mvprintw(5,5,"Hello World!"); 77 | refresh(); 78 | getch(); 79 | endwin(); 80 | return EXIT_SUCCESS; 81 | } 82 | ``` 83 | Result: 84 | 85 | ![Not super exciting](images/hello4.png) 86 | 87 | See? We've moved the hello world down 5 lines down and 5 characters over. 88 | 89 | ### Terminal animation with ncurses 90 | 91 | Now lets do something more interesting. We'll make a ball (the character 'o') and have it bounce off the walls of the terminal window. 92 | 93 | We'll create a new file BouncingO.c, and add in two structs to hold our data. 94 | 95 | ``` 96 | // Stores our balls position 97 | typedef struct _Ball { 98 | int x; 99 | int y; 100 | } Ball; 101 | // Stores the direction that the ball is moving 102 | typedef struct _Direction { 103 | int x; 104 | int y; 105 | } Direction; 106 | ``` 107 | 108 | Initialize with our ncurses functions 109 | ``` 110 | initscr(); // Start ncurses 111 | nodelay(stdscr, TRUE); // Don't wait for \n for getch to parse input 112 | cbreak(); // Switch off input buffering 113 | curs_set(FALSE); // Don't place a cursor on the screen 114 | ``` 115 | Initialize our ball and window information 116 | ``` 117 | int height = 0; // Window height 118 | int width = 0; // Window width 119 | 120 | // initialize our ball struct and direction struct 121 | Ball b = { 122 | 1,1 123 | }; 124 | Direction d = { 125 | 1,1 126 | }; 127 | 128 | // Get terminal window dimensions (rows and characters) 129 | getmaxyx(stdscr, height, width); 130 | ``` 131 | Note: getmaxyx() does not get passed variable addresses, but it does change the variables you pass in. 132 | 133 | And finally, make our "game" loop: 134 | 135 | ``` 136 | while (getch() == ERR) { 137 | // print ball 138 | mvprintw(b.y,b.x,"o"); 139 | 140 | // move ball for next frame 141 | b.y += d.y; 142 | b.x += d.x; 143 | 144 | // Check for ball being outside of our window boundaries 145 | if (b.x == width - 1 || b.x == 0){ 146 | // change direction 147 | d.x *= -1; 148 | b.x += d.x; 149 | } 150 | if (b.y == height - 1 || b.y == 0){ 151 | // change direction 152 | d.y *= -1; 153 | b.y += d.y; 154 | } 155 | 156 | refresh(); // Refresh the output 157 | usleep(50000); // Sleep to show output (Single frame) 158 | clear(); // Clear output 159 | } 160 | ``` 161 | Most of this is self explanatory but its worth noting that refresh() and clear() are ncurses functions and usleep is a unistd.h function. The "game" logic of the ball is just adding the direction -1 or 1 to the ball each frame, and changing the direction when it contacts the edge. 162 | 163 | Result: 164 | 165 | ![Bouncing Ball](https://thumbs.gfycat.com/FailingDownrightHippopotamus-size_restricted.gif) 166 | 167 | Fun Right? 168 | 169 | ### Colors 170 | Any self respecting game dev needs to use colors, ncurses makes this easy to do in the terminal. Because of the way that terminals work with color this has to be done with _color pairs_. 1 is the id of this color pair. 171 | 172 | ``` 173 | init_pair(1, COLOR_RED, COLOR_BLACK); // Always start at 0 174 | // This makes a red foreground on a black background 175 | ``` 176 | 177 | You then use it like this: 178 | ``` 179 | attron(COLOR_PAIR(1)); // Enable the color pair 180 | // Print something 181 | attroff(COLOR_PAIR(1)); // Its good practice to disable it 182 | ``` 183 | This is nice but its hard to use easily, I find it best to abstract this process. Here's an example for a colored, multi-ball version of the bouncingO program. 184 | 185 | This function abstracts away the color pair creation process. Basically, ncurses deals with colors as color pairs only, foreground and background. Since I only want to color the balls, I used -1 as the background (you MUST call use_default_colors() before doing this). Since I don't care which colors are which I'm just using a for loop to make all the possible color pairs with a blank background. When making your own program you may want to use just a single or a couple color pairs. You would make those like this: init_pair(ID, COLOR_RED, COLOR_WHITE); That will have a red letter on a white background. The IDs must start at 1 and go up. 186 | 187 | ``` 188 | void makeColorPairs(){ 189 | for (int i = 0; i < 8; i++) { 190 | init_pair(i+1, i, -1); 191 | } 192 | ``` 193 | Then I use them by randomly selecting a color pair 1-8 for the balls. It looks like this after combining with randomized initial positions: 194 | 195 | ![Bouncing Balls](https://thumbs.gfycat.com/PortlyVacantDutchshepherddog-size_restricted.gif) 196 | 197 | Here are the available colors in ncurses: 198 | ``` 199 | COLOR_BLACK 0 200 | COLOR_RED 1 201 | COLOR_GREEN 2 202 | COLOR_YELLOW 3 203 | COLOR_BLUE 4 204 | COLOR_MAGENTA 5 205 | COLOR_CYAN 6 206 | COLOR_WHITE 7 207 | ``` 208 | 209 | 210 | ## Making a larger project 211 | 212 | Hopefully now you should be able to see how you could make a terminal project, but I'm going to run through how I made the small terminal screensaver shown above. 213 | 214 | Repo here: [Bouncing Balls Repo](https://github.com/harrinp/BouncingBalls) 215 | 216 | When making a full-blown game my strategy is to abstract these printing functions so that the game logic is separate from the printing functions. In c this means that I create a main function that calls all of my logic and printing functions, but I keep any printing outside of my logic functions. Here's my main for the bouncing balls screensaver: 217 | 218 | ``` 219 | int main(int argc, char *argv[]) { 220 | int numBalls = 100; // If theres an argument process it 221 | if (argc > 1){ 222 | numBalls = atoi(argv[1]); 223 | // Theres no safeguard for bad input (because I'm lazy) don't try this at home 224 | } 225 | srand(time(NULL)); 226 | initscr(); // Start ncurses 227 | nodelay(stdscr, TRUE); // Don't wait for \n for getch to parse input 228 | cbreak(); // Switch off input buffering 229 | curs_set(FALSE); // Don't place a cursor on the screen 230 | start_color(); 231 | use_default_colors(); 232 | 233 | makeColorPairs(); // Abstracted color pair function 234 | 235 | Window w = initWindow(numBalls, 50000); // Initialize data structs 236 | 237 | while (getch() == ERR) { 238 | clear(); // Clear screen 239 | moveBalls(&w); // Movement logic 240 | printWindow(&w); // Print the balls 241 | usleep(w.sleepTime); // Sleep to show screen 242 | } 243 | destroyWindow(&w); 244 | nodelay(stdscr, FALSE); 245 | nocbreak(); 246 | endwin(); 247 | return 0; 248 | } 249 | 250 | ``` 251 | moveBalls() and printWindow() are abstracted from each other. One big benefit of this is that you can then multithread these two processes so that the game logic (or input for a game) can be processed while the print function sleeps. I did this for [walled in](https://github.com/harrinp/WalledIn) which gave the program much better performance than it otherwise would have had. 252 | 253 | the `while (getch() == ERR){}` waits for any key press and then ends the program if it gets one. 254 | 255 | Important to this strategy is using structs to hold your data, this allows you to keep the printing and logic separate - just pass the address (a pointer) of the game data struct(s) around and manipulate/read them for logic/printing. 256 | 257 | For this project my print function looked like this: 258 | ``` 259 | void printWindow(Window * w) { 260 | for (int i = 0; i < w->numBalls; i++) { 261 | attron(COLOR_PAIR(w->balls[i].colorPair)); // Applying the ball's color 262 | mvprintw(w->balls[i].y, w->balls[i].x, w->balls[i].avatar); 263 | attroff(COLOR_PAIR(w->balls[i].colorPair)); // Stop printing in this color 264 | } 265 | } 266 | ``` 267 | 268 | And here are the structs: 269 | ``` 270 | typedef struct _Ball{ 271 | // Position 272 | double x; 273 | double y; 274 | // How we display the ball 275 | char * avatar; 276 | // Speed (What we add to the x and y each loop iteration) 277 | double speedX; 278 | double speedY; 279 | 280 | int colorPair; 281 | } Ball; 282 | 283 | typedef struct _Window { 284 | int height; 285 | int width; 286 | int sleepTime; 287 | int numBalls; 288 | Ball * balls; 289 | } Window; 290 | ``` 291 | 292 | ## Conclusion 293 | Hopefully this guide successfully illustrated how to make games for the terminal, and good strategy for constructing them. If you have any questions or suggestions please contact me! 294 | -------------------------------------------------------------------------------- /examples/BouncingO.c: -------------------------------------------------------------------------------- 1 | #include "stdio.h" 2 | #include "stdlib.h" 3 | #include "unistd.h" 4 | #include "ncurses.h" 5 | 6 | // Stores our balls position 7 | typedef struct _Ball { 8 | int x; 9 | int y; 10 | } Ball; 11 | // Stores the direction that the ball is moving 12 | typedef struct _Direction { 13 | int x; 14 | int y; 15 | } Direction; 16 | 17 | int main() { 18 | initscr(); // Start ncurses 19 | nodelay(stdscr, TRUE); // Don't wait for \n for getch to parse input 20 | cbreak(); // Switch off input buffering 21 | curs_set(FALSE); // Don't place a cursor on the screen 22 | 23 | int height = 0; // Window height 24 | int width = 0; // Window width 25 | 26 | // initialize our ball struct and direction struct 27 | Ball b = { 28 | 1,1 29 | }; 30 | Direction d = { 31 | 1,1 32 | }; 33 | 34 | // Get terminal window dimensions (rows and characters) 35 | getmaxyx(stdscr, height, width); 36 | 37 | while (getch() == ERR) { 38 | // print ball 39 | mvprintw(b.y,b.x,"o"); 40 | 41 | // move ball for next frame 42 | b.y += d.y; 43 | b.x += d.x; 44 | 45 | // Check for ball being outside of our window boundaries 46 | if (b.x == width - 1 || b.x == 0){ 47 | // change direction 48 | d.x *= -1; 49 | b.x += d.x; 50 | } 51 | if (b.y == height - 1 || b.y == 0){ 52 | // change direction 53 | d.y *= -1; 54 | b.y += d.y; 55 | } 56 | 57 | refresh(); // Refresh the output 58 | usleep(50000); // Sleep to show output (Single frame) 59 | clear(); // Clear output 60 | } 61 | destroyWindow(&w); 62 | endwin(); 63 | return EXIT_SUCCESS; 64 | } 65 | -------------------------------------------------------------------------------- /examples/helloWorld.c: -------------------------------------------------------------------------------- 1 | #include "stdio.h" 2 | #include "stdlib.h" 3 | 4 | int main() { 5 | printf("Hello World!\n"); 6 | return EXIT_SUCCESS; 7 | } 8 | -------------------------------------------------------------------------------- /examples/helloWorldNew.c: -------------------------------------------------------------------------------- 1 | #include "stdio.h" 2 | #include "stdlib.h" 3 | #include "ncurses.h" 4 | 5 | int main() { 6 | initscr(); 7 | mvprintw(5,5,"Hello World!"); 8 | refresh(); 9 | getch(); 10 | endwin(); 11 | return EXIT_SUCCESS; 12 | } 13 | -------------------------------------------------------------------------------- /images/hello1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrinp/Command-line-guide/f3b33d96a99d58b5f27df9ef84ebb7d84d680176/images/hello1.png -------------------------------------------------------------------------------- /images/hello3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrinp/Command-line-guide/f3b33d96a99d58b5f27df9ef84ebb7d84d680176/images/hello3.png -------------------------------------------------------------------------------- /images/hello4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/harrinp/Command-line-guide/f3b33d96a99d58b5f27df9ef84ebb7d84d680176/images/hello4.png --------------------------------------------------------------------------------