terminal.nim 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. #
  2. #
  3. # Nim's Runtime Library
  4. # (c) Copyright 2012 Andreas Rumpf
  5. #
  6. # See the file "copying.txt", included in this
  7. # distribution, for details about the copyright.
  8. #
  9. ## This module contains a few procedures to control the *terminal*
  10. ## (also called *console*). On UNIX, the implementation simply uses ANSI escape
  11. ## sequences and does not depend on any other module, on Windows it uses the
  12. ## Windows API.
  13. ## Changing the style is permanent even after program termination! Use the
  14. ## code ``system.addQuitProc(resetAttributes)`` to restore the defaults.
  15. ## Similarly, if you hide the cursor, make sure to unhide it with
  16. ## ``showCursor`` before quitting.
  17. import macros
  18. when defined(windows):
  19. import winlean, os
  20. const
  21. DUPLICATE_SAME_ACCESS = 2
  22. FOREGROUND_BLUE = 1
  23. FOREGROUND_GREEN = 2
  24. FOREGROUND_RED = 4
  25. FOREGROUND_INTENSITY = 8
  26. BACKGROUND_BLUE = 16
  27. BACKGROUND_GREEN = 32
  28. BACKGROUND_RED = 64
  29. BACKGROUND_INTENSITY = 128
  30. FOREGROUND_RGB = FOREGROUND_RED or FOREGROUND_GREEN or FOREGROUND_BLUE
  31. BACKGROUND_RGB = BACKGROUND_RED or BACKGROUND_GREEN or BACKGROUND_BLUE
  32. type
  33. SHORT = int16
  34. COORD = object
  35. X: SHORT
  36. Y: SHORT
  37. SMALL_RECT = object
  38. Left: SHORT
  39. Top: SHORT
  40. Right: SHORT
  41. Bottom: SHORT
  42. CONSOLE_SCREEN_BUFFER_INFO = object
  43. dwSize: COORD
  44. dwCursorPosition: COORD
  45. wAttributes: int16
  46. srWindow: SMALL_RECT
  47. dwMaximumWindowSize: COORD
  48. CONSOLE_CURSOR_INFO = object
  49. dwSize: DWORD
  50. bVisible: WINBOOL
  51. proc duplicateHandle(hSourceProcessHandle: HANDLE, hSourceHandle: HANDLE,
  52. hTargetProcessHandle: HANDLE, lpTargetHandle: ptr HANDLE,
  53. dwDesiredAccess: DWORD, bInheritHandle: WINBOOL,
  54. dwOptions: DWORD): WINBOOL{.stdcall, dynlib: "kernel32",
  55. importc: "DuplicateHandle".}
  56. proc getCurrentProcess(): HANDLE{.stdcall, dynlib: "kernel32",
  57. importc: "GetCurrentProcess".}
  58. proc getConsoleScreenBufferInfo(hConsoleOutput: HANDLE,
  59. lpConsoleScreenBufferInfo: ptr CONSOLE_SCREEN_BUFFER_INFO): WINBOOL{.stdcall,
  60. dynlib: "kernel32", importc: "GetConsoleScreenBufferInfo".}
  61. proc getConsoleCursorInfo(hConsoleOutput: HANDLE,
  62. lpConsoleCursorInfo: ptr CONSOLE_CURSOR_INFO): WINBOOL{.
  63. stdcall, dynlib: "kernel32", importc: "GetConsoleCursorInfo".}
  64. proc setConsoleCursorInfo(hConsoleOutput: HANDLE,
  65. lpConsoleCursorInfo: ptr CONSOLE_CURSOR_INFO): WINBOOL{.
  66. stdcall, dynlib: "kernel32", importc: "SetConsoleCursorInfo".}
  67. proc terminalWidthIoctl*(handles: openArray[Handle]): int =
  68. var csbi: CONSOLE_SCREEN_BUFFER_INFO
  69. for h in handles:
  70. if getConsoleScreenBufferInfo(h, addr csbi) != 0:
  71. return int(csbi.srWindow.Right - csbi.srWindow.Left + 1)
  72. return 0
  73. proc terminalHeightIoctl*(handles: openArray[Handle]): int =
  74. var csbi: CONSOLE_SCREEN_BUFFER_INFO
  75. for h in handles:
  76. if getConsoleScreenBufferInfo(h, addr csbi) != 0:
  77. return int(csbi.srWindow.Bottom - csbi.srWindow.Top + 1)
  78. return 0
  79. proc terminalWidth*(): int =
  80. var w: int = 0
  81. w = terminalWidthIoctl([ getStdHandle(STD_INPUT_HANDLE),
  82. getStdHandle(STD_OUTPUT_HANDLE),
  83. getStdHandle(STD_ERROR_HANDLE) ] )
  84. if w > 0: return w
  85. return 80
  86. proc terminalHeight*(): int =
  87. var h: int = 0
  88. h = terminalHeightIoctl([ getStdHandle(STD_INPUT_HANDLE),
  89. getStdHandle(STD_OUTPUT_HANDLE),
  90. getStdHandle(STD_ERROR_HANDLE) ] )
  91. if h > 0: return h
  92. return 0
  93. proc setConsoleCursorPosition(hConsoleOutput: HANDLE,
  94. dwCursorPosition: COORD): WINBOOL{.
  95. stdcall, dynlib: "kernel32", importc: "SetConsoleCursorPosition".}
  96. proc fillConsoleOutputCharacter(hConsoleOutput: Handle, cCharacter: char,
  97. nLength: DWORD, dwWriteCoord: Coord,
  98. lpNumberOfCharsWritten: ptr DWORD): WINBOOL{.
  99. stdcall, dynlib: "kernel32", importc: "FillConsoleOutputCharacterA".}
  100. proc fillConsoleOutputAttribute(hConsoleOutput: HANDLE, wAttribute: int16,
  101. nLength: DWORD, dwWriteCoord: COORD,
  102. lpNumberOfAttrsWritten: ptr DWORD): WINBOOL{.
  103. stdcall, dynlib: "kernel32", importc: "FillConsoleOutputAttribute".}
  104. proc setConsoleTextAttribute(hConsoleOutput: HANDLE,
  105. wAttributes: int16): WINBOOL{.
  106. stdcall, dynlib: "kernel32", importc: "SetConsoleTextAttribute".}
  107. var
  108. hStdout: Handle # = createFile("CONOUT$", GENERIC_WRITE, 0, nil,
  109. # OPEN_ALWAYS, 0, 0)
  110. hStderr: Handle
  111. block:
  112. var hStdoutTemp = getStdHandle(STD_OUTPUT_HANDLE)
  113. if duplicateHandle(getCurrentProcess(), hStdoutTemp, getCurrentProcess(),
  114. addr(hStdout), 0, 1, DUPLICATE_SAME_ACCESS) == 0:
  115. when defined(consoleapp):
  116. raiseOSError(osLastError())
  117. var hStderrTemp = getStdHandle(STD_ERROR_HANDLE)
  118. if duplicateHandle(getCurrentProcess(), hStderrTemp, getCurrentProcess(),
  119. addr(hStderr), 0, 1, DUPLICATE_SAME_ACCESS) == 0:
  120. when defined(consoleapp):
  121. raiseOSError(osLastError())
  122. proc getCursorPos(h: Handle): tuple [x,y: int] =
  123. var c: CONSOLESCREENBUFFERINFO
  124. if getConsoleScreenBufferInfo(h, addr(c)) == 0:
  125. raiseOSError(osLastError())
  126. return (int(c.dwCursorPosition.X), int(c.dwCursorPosition.Y))
  127. proc setCursorPos(h: Handle, x, y: int) =
  128. var c: COORD
  129. c.X = int16(x)
  130. c.Y = int16(y)
  131. if setConsoleCursorPosition(h, c) == 0:
  132. raiseOSError(osLastError())
  133. proc getAttributes(h: Handle): int16 =
  134. var c: CONSOLESCREENBUFFERINFO
  135. # workaround Windows bugs: try several times
  136. if getConsoleScreenBufferInfo(h, addr(c)) != 0:
  137. return c.wAttributes
  138. return 0x70'i16 # ERROR: return white background, black text
  139. var
  140. oldStdoutAttr = getAttributes(hStdout)
  141. oldStderrAttr = getAttributes(hStderr)
  142. template conHandle(f: File): Handle =
  143. if f == stderr: hStderr else: hStdout
  144. else:
  145. import termios, posix, os, parseutils
  146. proc setRaw(fd: FileHandle, time: cint = TCSAFLUSH) =
  147. var mode: Termios
  148. discard fd.tcgetattr(addr mode)
  149. mode.c_iflag = mode.c_iflag and not Cflag(BRKINT or ICRNL or INPCK or
  150. ISTRIP or IXON)
  151. mode.c_oflag = mode.c_oflag and not Cflag(OPOST)
  152. mode.c_cflag = (mode.c_cflag and not Cflag(CSIZE or PARENB)) or CS8
  153. mode.c_lflag = mode.c_lflag and not Cflag(ECHO or ICANON or IEXTEN or ISIG)
  154. mode.c_cc[VMIN] = 1.cuchar
  155. mode.c_cc[VTIME] = 0.cuchar
  156. discard fd.tcsetattr(time, addr mode)
  157. proc terminalWidthIoctl*(fds: openArray[int]): int =
  158. ## Returns terminal width from first fd that supports the ioctl.
  159. var win: IOctl_WinSize
  160. for fd in fds:
  161. if ioctl(cint(fd), TIOCGWINSZ, addr win) != -1:
  162. return int(win.ws_col)
  163. return 0
  164. proc terminalHeightIoctl*(fds: openArray[int]): int =
  165. ## Returns terminal height from first fd that supports the ioctl.
  166. var win: IOctl_WinSize
  167. for fd in fds:
  168. if ioctl(cint(fd), TIOCGWINSZ, addr win) != -1:
  169. return int(win.ws_row)
  170. return 0
  171. var L_ctermid{.importc, header: "<stdio.h>".}: cint
  172. proc terminalWidth*(): int =
  173. ## Returns some reasonable terminal width from either standard file
  174. ## descriptors, controlling terminal, environment variables or tradition.
  175. var w = terminalWidthIoctl([0, 1, 2]) #Try standard file descriptors
  176. if w > 0: return w
  177. var cterm = newString(L_ctermid) #Try controlling tty
  178. var fd = open(ctermid(cstring(cterm)), O_RDONLY)
  179. if fd != -1:
  180. w = terminalWidthIoctl([ int(fd) ])
  181. discard close(fd)
  182. if w > 0: return w
  183. var s = getEnv("COLUMNS") #Try standard env var
  184. if len(s) > 0 and parseInt(string(s), w) > 0 and w > 0:
  185. return w
  186. return 80 #Finally default to venerable value
  187. proc terminalHeight*(): int =
  188. ## Returns some reasonable terminal height from either standard file
  189. ## descriptors, controlling terminal, environment variables or tradition.
  190. ## Zero is returned if the height could not be determined.
  191. var h = terminalHeightIoctl([0, 1, 2]) # Try standard file descriptors
  192. if h > 0: return h
  193. var cterm = newString(L_ctermid) # Try controlling tty
  194. var fd = open(ctermid(cstring(cterm)), O_RDONLY)
  195. if fd != -1:
  196. h = terminalHeightIoctl([ int(fd) ])
  197. discard close(fd)
  198. if h > 0: return h
  199. var s = getEnv("LINES") # Try standard env var
  200. if len(s) > 0 and parseInt(string(s), h) > 0 and h > 0:
  201. return h
  202. return 0 # Could not determine height
  203. proc terminalSize*(): tuple[w, h: int] =
  204. ## Returns the terminal width and height as a tuple. Internally calls
  205. ## `terminalWidth` and `terminalHeight`, so the same assumptions apply.
  206. result = (terminalWidth(), terminalHeight())
  207. when defined(windows):
  208. proc setCursorVisibility(f: File, visible: bool) =
  209. var ccsi: CONSOLE_CURSOR_INFO
  210. let h = conHandle(f)
  211. if getConsoleCursorInfo(h, addr(ccsi)) == 0:
  212. raiseOSError(osLastError())
  213. ccsi.bVisible = if visible: 1 else: 0
  214. if setConsoleCursorInfo(h, addr(ccsi)) == 0:
  215. raiseOSError(osLastError())
  216. proc hideCursor*(f: File) =
  217. ## Hides the cursor.
  218. when defined(windows):
  219. setCursorVisibility(f, false)
  220. else:
  221. f.write("\e[?25l")
  222. proc showCursor*(f: File) =
  223. ## Shows the cursor.
  224. when defined(windows):
  225. setCursorVisibility(f, true)
  226. else:
  227. f.write("\e[?25h")
  228. proc setCursorPos*(f: File, x, y: int) =
  229. ## Sets the terminal's cursor to the (x,y) position.
  230. ## (0,0) is the upper left of the screen.
  231. when defined(windows):
  232. let h = conHandle(f)
  233. setCursorPos(h, x, y)
  234. else:
  235. f.write("\e[" & $y & ';' & $x & 'f')
  236. proc setCursorXPos*(f: File, x: int) =
  237. ## Sets the terminal's cursor to the x position.
  238. ## The y position is not changed.
  239. when defined(windows):
  240. let h = conHandle(f)
  241. var scrbuf: CONSOLESCREENBUFFERINFO
  242. if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0:
  243. raiseOSError(osLastError())
  244. var origin = scrbuf.dwCursorPosition
  245. origin.X = int16(x)
  246. if setConsoleCursorPosition(h, origin) == 0:
  247. raiseOSError(osLastError())
  248. else:
  249. f.write("\e[" & $x & 'G')
  250. when defined(windows):
  251. proc setCursorYPos*(f: File, y: int) =
  252. ## Sets the terminal's cursor to the y position.
  253. ## The x position is not changed.
  254. ## **Warning**: This is not supported on UNIX!
  255. when defined(windows):
  256. let h = conHandle(f)
  257. var scrbuf: CONSOLESCREENBUFFERINFO
  258. if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0:
  259. raiseOSError(osLastError())
  260. var origin = scrbuf.dwCursorPosition
  261. origin.Y = int16(y)
  262. if setConsoleCursorPosition(h, origin) == 0:
  263. raiseOSError(osLastError())
  264. else:
  265. discard
  266. proc cursorUp*(f: File, count=1) =
  267. ## Moves the cursor up by `count` rows.
  268. when defined(windows):
  269. let h = conHandle(f)
  270. var p = getCursorPos(h)
  271. dec(p.y, count)
  272. setCursorPos(h, p.x, p.y)
  273. else:
  274. f.write("\e[" & $count & 'A')
  275. proc cursorDown*(f: File, count=1) =
  276. ## Moves the cursor down by `count` rows.
  277. when defined(windows):
  278. let h = conHandle(f)
  279. var p = getCursorPos(h)
  280. inc(p.y, count)
  281. setCursorPos(h, p.x, p.y)
  282. else:
  283. f.write("\e[" & $count & 'B')
  284. proc cursorForward*(f: File, count=1) =
  285. ## Moves the cursor forward by `count` columns.
  286. when defined(windows):
  287. let h = conHandle(f)
  288. var p = getCursorPos(h)
  289. inc(p.x, count)
  290. setCursorPos(h, p.x, p.y)
  291. else:
  292. f.write("\e[" & $count & 'C')
  293. proc cursorBackward*(f: File, count=1) =
  294. ## Moves the cursor backward by `count` columns.
  295. when defined(windows):
  296. let h = conHandle(f)
  297. var p = getCursorPos(h)
  298. dec(p.x, count)
  299. setCursorPos(h, p.x, p.y)
  300. else:
  301. f.write("\e[" & $count & 'D')
  302. when true:
  303. discard
  304. else:
  305. proc eraseLineEnd*(f: File) =
  306. ## Erases from the current cursor position to the end of the current line.
  307. when defined(windows):
  308. discard
  309. else:
  310. f.write("\e[K")
  311. proc eraseLineStart*(f: File) =
  312. ## Erases from the current cursor position to the start of the current line.
  313. when defined(windows):
  314. discard
  315. else:
  316. f.write("\e[1K")
  317. proc eraseDown*(f: File) =
  318. ## Erases the screen from the current line down to the bottom of the screen.
  319. when defined(windows):
  320. discard
  321. else:
  322. f.write("\e[J")
  323. proc eraseUp*(f: File) =
  324. ## Erases the screen from the current line up to the top of the screen.
  325. when defined(windows):
  326. discard
  327. else:
  328. f.write("\e[1J")
  329. proc eraseLine*(f: File) =
  330. ## Erases the entire current line.
  331. when defined(windows):
  332. let h = conHandle(f)
  333. var scrbuf: CONSOLESCREENBUFFERINFO
  334. var numwrote: DWORD
  335. if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0:
  336. raiseOSError(osLastError())
  337. var origin = scrbuf.dwCursorPosition
  338. origin.X = 0'i16
  339. if setConsoleCursorPosition(h, origin) == 0:
  340. raiseOSError(osLastError())
  341. var ht = scrbuf.dwSize.Y - origin.Y
  342. var wt = scrbuf.dwSize.X - origin.X
  343. if fillConsoleOutputCharacter(h, ' ', ht*wt,
  344. origin, addr(numwrote)) == 0:
  345. raiseOSError(osLastError())
  346. if fillConsoleOutputAttribute(h, scrbuf.wAttributes, ht * wt,
  347. scrbuf.dwCursorPosition, addr(numwrote)) == 0:
  348. raiseOSError(osLastError())
  349. else:
  350. f.write("\e[2K")
  351. setCursorXPos(f, 0)
  352. proc eraseScreen*(f: File) =
  353. ## Erases the screen with the background colour and moves the cursor to home.
  354. when defined(windows):
  355. let h = conHandle(f)
  356. var scrbuf: CONSOLESCREENBUFFERINFO
  357. var numwrote: DWORD
  358. var origin: COORD # is inititalized to 0, 0
  359. if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0:
  360. raiseOSError(osLastError())
  361. let numChars = int32(scrbuf.dwSize.X)*int32(scrbuf.dwSize.Y)
  362. if fillConsoleOutputCharacter(h, ' ', numChars,
  363. origin, addr(numwrote)) == 0:
  364. raiseOSError(osLastError())
  365. if fillConsoleOutputAttribute(h, scrbuf.wAttributes, numChars,
  366. origin, addr(numwrote)) == 0:
  367. raiseOSError(osLastError())
  368. setCursorXPos(f, 0)
  369. else:
  370. f.write("\e[2J")
  371. proc resetAttributes*(f: File) =
  372. ## Resets all attributes.
  373. when defined(windows):
  374. if f == stderr:
  375. discard setConsoleTextAttribute(hStderr, oldStderrAttr)
  376. else:
  377. discard setConsoleTextAttribute(hStdout, oldStdoutAttr)
  378. else:
  379. f.write("\e[0m")
  380. type
  381. Style* = enum ## different styles for text output
  382. styleBright = 1, ## bright text
  383. styleDim, ## dim text
  384. styleUnknown, ## unknown
  385. styleUnderscore = 4, ## underscored text
  386. styleBlink, ## blinking/bold text
  387. styleReverse = 7, ## unknown
  388. styleHidden ## hidden text
  389. {.deprecated: [TStyle: Style].}
  390. when not defined(windows):
  391. var
  392. # XXX: These better be thread-local
  393. gFG = 0
  394. gBG = 0
  395. proc setStyle*(f: File, style: set[Style]) =
  396. ## Sets the terminal style.
  397. when defined(windows):
  398. let h = conHandle(f)
  399. var old = getAttributes(h) and (FOREGROUND_RGB or BACKGROUND_RGB)
  400. var a = 0'i16
  401. if styleBright in style: a = a or int16(FOREGROUND_INTENSITY)
  402. if styleBlink in style: a = a or int16(BACKGROUND_INTENSITY)
  403. if styleReverse in style: a = a or 0x4000'i16 # COMMON_LVB_REVERSE_VIDEO
  404. if styleUnderscore in style: a = a or 0x8000'i16 # COMMON_LVB_UNDERSCORE
  405. discard setConsoleTextAttribute(h, old or a)
  406. else:
  407. for s in items(style):
  408. f.write("\e[" & $ord(s) & 'm')
  409. proc writeStyled*(txt: string, style: set[Style] = {styleBright}) =
  410. ## Writes the text `txt` in a given `style` to stdout.
  411. when defined(windows):
  412. var old = getAttributes(hStdout)
  413. stdout.setStyle(style)
  414. stdout.write(txt)
  415. discard setConsoleTextAttribute(hStdout, old)
  416. else:
  417. stdout.setStyle(style)
  418. stdout.write(txt)
  419. stdout.resetAttributes()
  420. if gFG != 0:
  421. stdout.write("\e[" & $ord(gFG) & 'm')
  422. if gBG != 0:
  423. stdout.write("\e[" & $ord(gBG) & 'm')
  424. type
  425. ForegroundColor* = enum ## terminal's foreground colors
  426. fgBlack = 30, ## black
  427. fgRed, ## red
  428. fgGreen, ## green
  429. fgYellow, ## yellow
  430. fgBlue, ## blue
  431. fgMagenta, ## magenta
  432. fgCyan, ## cyan
  433. fgWhite ## white
  434. BackgroundColor* = enum ## terminal's background colors
  435. bgBlack = 40, ## black
  436. bgRed, ## red
  437. bgGreen, ## green
  438. bgYellow, ## yellow
  439. bgBlue, ## blue
  440. bgMagenta, ## magenta
  441. bgCyan, ## cyan
  442. bgWhite ## white
  443. {.deprecated: [TForegroundColor: ForegroundColor,
  444. TBackgroundColor: BackgroundColor].}
  445. proc setForegroundColor*(f: File, fg: ForegroundColor, bright=false) =
  446. ## Sets the terminal's foreground color.
  447. when defined(windows):
  448. let h = conHandle(f)
  449. var old = getAttributes(h) and not FOREGROUND_RGB
  450. if bright:
  451. old = old or FOREGROUND_INTENSITY
  452. const lookup: array[ForegroundColor, int] = [
  453. 0,
  454. (FOREGROUND_RED),
  455. (FOREGROUND_GREEN),
  456. (FOREGROUND_RED or FOREGROUND_GREEN),
  457. (FOREGROUND_BLUE),
  458. (FOREGROUND_RED or FOREGROUND_BLUE),
  459. (FOREGROUND_BLUE or FOREGROUND_GREEN),
  460. (FOREGROUND_BLUE or FOREGROUND_GREEN or FOREGROUND_RED)]
  461. discard setConsoleTextAttribute(h, toU16(old or lookup[fg]))
  462. else:
  463. gFG = ord(fg)
  464. if bright: inc(gFG, 60)
  465. f.write("\e[" & $gFG & 'm')
  466. proc setBackgroundColor*(f: File, bg: BackgroundColor, bright=false) =
  467. ## Sets the terminal's background color.
  468. when defined(windows):
  469. let h = conHandle(f)
  470. var old = getAttributes(h) and not BACKGROUND_RGB
  471. if bright:
  472. old = old or BACKGROUND_INTENSITY
  473. const lookup: array[BackgroundColor, int] = [
  474. 0,
  475. (BACKGROUND_RED),
  476. (BACKGROUND_GREEN),
  477. (BACKGROUND_RED or BACKGROUND_GREEN),
  478. (BACKGROUND_BLUE),
  479. (BACKGROUND_RED or BACKGROUND_BLUE),
  480. (BACKGROUND_BLUE or BACKGROUND_GREEN),
  481. (BACKGROUND_BLUE or BACKGROUND_GREEN or BACKGROUND_RED)]
  482. discard setConsoleTextAttribute(h, toU16(old or lookup[bg]))
  483. else:
  484. gBG = ord(bg)
  485. if bright: inc(gBG, 60)
  486. f.write("\e[" & $gBG & 'm')
  487. proc isatty*(f: File): bool =
  488. ## Returns true if `f` is associated with a terminal device.
  489. when defined(posix):
  490. proc isatty(fildes: FileHandle): cint {.
  491. importc: "isatty", header: "<unistd.h>".}
  492. else:
  493. proc isatty(fildes: FileHandle): cint {.
  494. importc: "_isatty", header: "<io.h>".}
  495. result = isatty(getFileHandle(f)) != 0'i32
  496. type
  497. TerminalCmd* = enum ## commands that can be expressed as arguments
  498. resetStyle ## reset attributes
  499. template styledEchoProcessArg(f: File, s: string) = write f, s
  500. template styledEchoProcessArg(f: File, style: Style) = setStyle(f, {style})
  501. template styledEchoProcessArg(f: File, style: set[Style]) = setStyle f, style
  502. template styledEchoProcessArg(f: File, color: ForegroundColor) =
  503. setForegroundColor f, color
  504. template styledEchoProcessArg(f: File, color: BackgroundColor) =
  505. setBackgroundColor f, color
  506. template styledEchoProcessArg(f: File, cmd: TerminalCmd) =
  507. when cmd == resetStyle:
  508. resetAttributes(f)
  509. macro styledWriteLine*(f: File, m: varargs[typed]): untyped =
  510. ## Similar to ``writeLine``, but treating terminal style arguments specially.
  511. ## When some argument is ``Style``, ``set[Style]``, ``ForegroundColor``,
  512. ## ``BackgroundColor`` or ``TerminalCmd`` then it is not sent directly to
  513. ## ``f``, but instead corresponding terminal style proc is called.
  514. ##
  515. ## Example:
  516. ##
  517. ## .. code-block:: nim
  518. ##
  519. ## proc error(msg: string) =
  520. ## styledWriteLine(stderr, fgRed, "Error: ", resetStyle, msg)
  521. ##
  522. let m = callsite()
  523. var reset = false
  524. result = newNimNode(nnkStmtList)
  525. for i in countup(2, m.len - 1):
  526. let item = m[i]
  527. case item.kind
  528. of nnkStrLit..nnkTripleStrLit:
  529. if i == m.len - 1:
  530. # optimize if string literal is last, just call writeLine
  531. result.add(newCall(bindSym"writeLine", f, item))
  532. if reset: result.add(newCall(bindSym"resetAttributes", f))
  533. return
  534. else:
  535. # if it is string literal just call write, do not enable reset
  536. result.add(newCall(bindSym"write", f, item))
  537. else:
  538. result.add(newCall(bindSym"styledEchoProcessArg", f, item))
  539. reset = true
  540. result.add(newCall(bindSym"write", f, newStrLitNode("\n")))
  541. if reset: result.add(newCall(bindSym"resetAttributes", f))
  542. macro styledEcho*(args: varargs[untyped]): untyped =
  543. ## Echoes styles arguments to stdout using ``styledWriteLine``.
  544. result = newCall(bindSym"styledWriteLine")
  545. result.add(bindSym"stdout")
  546. for arg in children(args):
  547. result.add(arg)
  548. proc getch*(): char =
  549. ## Read a single character from the terminal, blocking until it is entered.
  550. ## The character is not printed to the terminal.
  551. when defined(windows):
  552. let fd = getStdHandle(STD_INPUT_HANDLE)
  553. var keyEvent = KEY_EVENT_RECORD()
  554. var numRead: cint
  555. while true:
  556. # Block until character is entered
  557. doAssert(waitForSingleObject(fd, INFINITE) == WAIT_OBJECT_0)
  558. doAssert(readConsoleInput(fd, addr(keyEvent), 1, addr(numRead)) != 0)
  559. if numRead == 0 or keyEvent.eventType != 1 or keyEvent.bKeyDown == 0:
  560. continue
  561. return char(keyEvent.uChar)
  562. else:
  563. let fd = getFileHandle(stdin)
  564. var oldMode: Termios
  565. discard fd.tcgetattr(addr oldMode)
  566. fd.setRaw()
  567. result = stdin.readChar()
  568. discard fd.tcsetattr(TCSADRAIN, addr oldMode)
  569. # Wrappers assuming output to stdout:
  570. template hideCursor*() = hideCursor(stdout)
  571. template showCursor*() = showCursor(stdout)
  572. template setCursorPos*(x, y: int) = setCursorPos(stdout, x, y)
  573. template setCursorXPos*(x: int) = setCursorXPos(stdout, x)
  574. when defined(windows):
  575. template setCursorYPos(x: int) = setCursorYPos(stdout, x)
  576. template cursorUp*(count=1) = cursorUp(stdout, f)
  577. template cursorDown*(count=1) = cursorDown(stdout, f)
  578. template cursorForward*(count=1) = cursorForward(stdout, f)
  579. template cursorBackward*(count=1) = cursorBackward(stdout, f)
  580. template eraseLine*() = eraseLine(stdout)
  581. template eraseScreen*() = eraseScreen(stdout)
  582. template setStyle*(style: set[Style]) =
  583. setStyle(stdout, style)
  584. template setForegroundColor*(fg: ForegroundColor, bright=false) =
  585. setForegroundColor(stdout, fg, bright)
  586. template setBackgroundColor*(bg: BackgroundColor, bright=false) =
  587. setBackgroundColor(stdout, bg, bright)
  588. proc resetAttributes*() {.noconv.} =
  589. ## Resets all attributes on stdout.
  590. ## It is advisable to register this as a quit proc with
  591. ## ``system.addQuitProc(resetAttributes)``.
  592. resetAttributes(stdout)
  593. when not defined(testing) and isMainModule:
  594. #system.addQuitProc(resetAttributes)
  595. write(stdout, "never mind")
  596. stdout.eraseLine()
  597. stdout.styledWriteLine("styled text ", {styleBright, styleBlink, styleUnderscore})
  598. stdout.setBackGroundColor(bgCyan, true)
  599. stdout.setForeGroundColor(fgBlue)
  600. stdout.writeLine("ordinary text")
  601. stdout.resetAttributes()