├── .gitignore ├── .editorconfig ├── binding.gyp ├── yarn.lock ├── package.json ├── LICENSE ├── README.md ├── index.js └── index.cc /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .vscode 4 | .DS_Store 5 | .vscode 6 | npm-debug.log 7 | yarn-error.log 8 | node_modules 9 | build 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | indent_style = space 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | 17 | [Makefile] 18 | indent_style = tab 19 | -------------------------------------------------------------------------------- /binding.gyp: -------------------------------------------------------------------------------- 1 | { 2 | "targets": [ 3 | { 4 | "target_name": "pos", 5 | "sources": [ "./index.cc" ], 6 | "cflags!": [ "-fno-exceptions" ], 7 | "cflags_cc!": [ "-fno-exceptions" ], 8 | "include_dirs": [ 9 | " Get the cursor's current position in your terminal. 4 | 5 | [![MIT License](https://img.shields.io/badge/license-MIT_License-green.svg?style=flat-square)](https://github.com/bubkoo/get-cursor-position/blob/master/LICENSE) 6 | [![Package Quality](http://npm.packagequality.com/shield/get-cursor-position.svg)](http://packagequality.com/#?package=get-cursor-position) 7 | 8 | ## Install 9 | 10 | ``` 11 | $ npm install get-cursor-position --save 12 | ``` 13 | 14 | ## Usage 15 | 16 | Async: 17 | 18 | ```js 19 | 20 | var getCursorPosition = require('get-cursor-position'); 21 | 22 | getCursorPosition.async(function(pos) { 23 | console.log('row: ' + pos.row); 24 | console.log('col: ' + pos.col); 25 | }); 26 | 27 | ``` 28 | 29 | Sync: 30 | 31 | ```js 32 | var getCursorPosition = require('get-cursor-position'); 33 | var pos = getCursorPosition.sync(); 34 | console.log('row: ' + pos.row); 35 | console.log('col: ' + pos.col); 36 | ``` 37 | 38 | ## Contributing 39 | 40 | Pull requests and stars are highly welcome. 41 | 42 | For bugs and feature requests, please [create an issue](https://github.com/bubkoo/get-cursor-position/issues/new). 43 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var tty = require('tty'); 2 | var pos = require('bindings')('pos'); 3 | var code = '\x1b[6n'; 4 | 5 | function setRawMode(mode) { 6 | if (process.stdin.setRawMode) { 7 | process.stdin.setRawMode(mode) 8 | } else { 9 | tty.setRawMode(mode) 10 | } 11 | } 12 | 13 | pos.async = function (callback, context) { 14 | if (process.platform === 'win32') { 15 | process.nextTick(function () { 16 | var position = pos.sync(); 17 | 18 | if (position) { 19 | callback && callback.call(context, { 20 | row: position.row, 21 | col: position.col 22 | }); 23 | } 24 | }); 25 | 26 | return; 27 | } 28 | 29 | // start listening 30 | process.stdin.resume(); 31 | setRawMode(true); 32 | 33 | process.stdin.once('data', function (b) { 34 | var match = /\[(\d+)\;(\d+)R$/.exec(b.toString()); 35 | if (match) { 36 | var position = match.slice(1, 3).reverse().map(Number); 37 | 38 | callback && callback.call(context, { 39 | row: position[1], 40 | col: position[0] 41 | }); 42 | } 43 | 44 | // cleanup and close stdin 45 | setRawMode(false); 46 | process.stdin.pause(); 47 | }); 48 | 49 | 50 | process.stdout.write(code); 51 | process.stdout.emit('data', code); 52 | }; 53 | 54 | module.exports = pos; 55 | 56 | -------------------------------------------------------------------------------- /index.cc: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | #ifdef _WIN32 4 | #include 5 | 6 | int cursor_position(int *const rowptr, int *const colptr) 7 | { 8 | HANDLE console_handle; 9 | CONSOLE_SCREEN_BUFFER_INFO console_info; 10 | 11 | console_handle = CreateFileW(L"CONOUT$", 12 | GENERIC_READ | GENERIC_WRITE, 13 | FILE_SHARE_READ | FILE_SHARE_WRITE, 14 | NULL, 15 | OPEN_EXISTING, 16 | FILE_ATTRIBUTE_NORMAL, 17 | NULL); 18 | 19 | if (console_handle == INVALID_HANDLE_VALUE) 20 | return GetLastError(); 21 | 22 | if (!GetConsoleScreenBufferInfo(console_handle, &console_info)) 23 | return GetLastError(); 24 | 25 | /* Success! */ 26 | 27 | if (rowptr) 28 | *rowptr = console_info.dwCursorPosition.Y + 1; 29 | 30 | if (colptr) 31 | *colptr = console_info.dwCursorPosition.X + 1; 32 | 33 | /* Done. */ 34 | return 0; 35 | } 36 | 37 | #else 38 | 39 | #include 40 | #include 41 | #include 42 | 43 | #define RD_EOF -1 44 | #define RD_EIO -2 45 | 46 | static inline int rd(const int fd) 47 | { 48 | unsigned char buffer[4]; 49 | ssize_t n; 50 | 51 | while (1) 52 | { 53 | n = read(fd, buffer, 1); 54 | 55 | if (n > (ssize_t)0) 56 | return buffer[0]; 57 | else if (n == (ssize_t)0) 58 | return RD_EOF; 59 | else if (n != (ssize_t)-1) 60 | return RD_EIO; 61 | else if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) 62 | return RD_EIO; 63 | } 64 | } 65 | 66 | static inline int wr(const int fd, const char *const data, const size_t bytes) 67 | { 68 | const char *head = data; 69 | const char *const tail = data + bytes; 70 | ssize_t n; 71 | 72 | while (head < tail) 73 | { 74 | 75 | n = write(fd, head, (size_t)(tail - head)); 76 | 77 | if (n > (ssize_t)0) 78 | head += n; 79 | else if (n != (ssize_t)-1) 80 | return EIO; 81 | else if (errno != EINTR && errno != EAGAIN && errno != EWOULDBLOCK) 82 | return errno; 83 | } 84 | 85 | return 0; 86 | } 87 | 88 | /* Return a new file descriptor to the current TTY. */ 89 | int current_tty(void) 90 | { 91 | const char *dev; 92 | int fd; 93 | 94 | dev = ttyname(STDIN_FILENO); 95 | 96 | if (!dev) 97 | dev = ttyname(STDOUT_FILENO); 98 | 99 | if (!dev) 100 | dev = ttyname(STDERR_FILENO); 101 | 102 | if (!dev) 103 | { 104 | errno = ENOTTY; 105 | return -1; 106 | } 107 | 108 | do 109 | { 110 | fd = open(dev, O_RDWR | O_NOCTTY); 111 | } while (fd == -1 && errno == EINTR); 112 | 113 | if (fd == -1) 114 | return -1; 115 | 116 | return fd; 117 | } 118 | 119 | /* As the tty for current cursor position. 120 | * This function returns 0 if success, errno code otherwise. 121 | * Actual errno will be unchanged. 122 | */ 123 | int cursor_position(int *const rowptr, int *const colptr) 124 | { 125 | struct termios saved, temporary; 126 | int tty, retval, result, rows, cols, saved_errno; 127 | 128 | tty = current_tty(); 129 | 130 | /* Bad tty? */ 131 | if (tty == -1) 132 | return ENOTTY; 133 | 134 | saved_errno = errno; 135 | 136 | /* Save current terminal settings. */ 137 | do 138 | { 139 | result = tcgetattr(tty, &saved); 140 | } while (result == -1 && errno == EINTR); 141 | 142 | if (result == -1) 143 | { 144 | retval = errno; 145 | errno = saved_errno; 146 | return retval; 147 | } 148 | 149 | /* Get current terminal settings for basis, too. */ 150 | do 151 | { 152 | result = tcgetattr(tty, &temporary); 153 | } while (result == -1 && errno == EINTR); 154 | 155 | if (result == -1) 156 | { 157 | retval = errno; 158 | errno = saved_errno; 159 | return retval; 160 | } 161 | 162 | /* Disable ICANON, ECHO, and CREAD. */ 163 | temporary.c_lflag &= ~ICANON; 164 | temporary.c_lflag &= ~ECHO; 165 | temporary.c_cflag &= ~CREAD; 166 | 167 | /* This loop is only executed once. When broken out, 168 | * the terminal settings will be restored, and the function 169 | * will return retval to caller. It's better than goto. 170 | */ 171 | do 172 | { 173 | /* Set modified settings. */ 174 | do 175 | { 176 | result = tcsetattr(tty, TCSANOW, &temporary); 177 | } while (result == -1 && errno == EINTR); 178 | 179 | if (result == -1) 180 | { 181 | retval = errno; 182 | break; 183 | } 184 | 185 | /* Request cursor coordinates from the terminal. */ 186 | retval = wr(tty, "\033[6n", 4); 187 | if (retval) 188 | break; 189 | 190 | /* Assume coordinate reponse parsing fails. */ 191 | retval = EIO; 192 | 193 | /* Expect an ESC. */ 194 | result = rd(tty); 195 | if (result != 27) 196 | break; 197 | 198 | /* Expect [ after the ESC. */ 199 | result = rd(tty); 200 | if (result != '[') 201 | break; 202 | 203 | /* Parse rows. */ 204 | rows = 0; 205 | result = rd(tty); 206 | while (result >= '0' && result <= '9') 207 | { 208 | rows = 10 * rows + result - '0'; 209 | result = rd(tty); 210 | } 211 | 212 | if (result != ';') 213 | break; 214 | 215 | /* Parse cols. */ 216 | cols = 0; 217 | result = rd(tty); 218 | while (result >= '0' && result <= '9') 219 | { 220 | cols = 10 * cols + result - '0'; 221 | result = rd(tty); 222 | } 223 | 224 | if (result != 'R') 225 | break; 226 | 227 | /* Success! */ 228 | 229 | if (rowptr) 230 | *rowptr = rows; 231 | 232 | if (colptr) 233 | *colptr = cols; 234 | 235 | retval = 0; 236 | 237 | } while (0); 238 | 239 | /* Restore saved terminal settings. */ 240 | do 241 | { 242 | result = tcsetattr(tty, TCSANOW, &saved); 243 | } while (result == -1 && errno == EINTR); 244 | 245 | if (result == -1 && !retval) 246 | retval = errno; 247 | 248 | /* Done. */ 249 | close(tty); 250 | return retval; 251 | } 252 | 253 | #endif 254 | 255 | Napi::Object Method(const Napi::CallbackInfo &info) 256 | { 257 | Napi::Env env = info.Env(); 258 | Napi::Object pos = Napi::Object::New(env); 259 | 260 | int row, col; 261 | 262 | row = 0; 263 | col = 0; 264 | 265 | if (cursor_position(&row, &col)) 266 | { 267 | return pos; 268 | } 269 | 270 | if (row < 1 || col < 1) 271 | { 272 | return pos; 273 | } 274 | 275 | pos.Set("row", row); 276 | pos.Set("col", col); 277 | 278 | return pos; 279 | } 280 | 281 | Napi::Object Init(Napi::Env env, Napi::Object exports) 282 | { 283 | exports.Set(Napi::String::New(env, "sync"), Napi::Function::New(env, Method)); 284 | return exports; 285 | } 286 | 287 | NODE_API_MODULE(pos, Init) 288 | --------------------------------------------------------------------------------