terminal.nim 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034
  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 `exitprocs.addExitProc(resetAttributes)` to restore the defaults.
  15. ## Similarly, if you hide the cursor, make sure to unhide it with
  16. ## `showCursor` before quitting.
  17. ##
  18. ## Progress bar
  19. ## ============
  20. ##
  21. ## Basic progress bar example:
  22. runnableExamples("-r:off"):
  23. import std/[os, strutils]
  24. for i in 0..100:
  25. stdout.styledWriteLine(fgRed, "0% ", fgWhite, '#'.repeat i, if i > 50: fgGreen else: fgYellow, "\t", $i , "%")
  26. sleep 42
  27. cursorUp 1
  28. eraseLine()
  29. stdout.resetAttributes()
  30. ##[
  31. ## Playing with colorful and styled text
  32. ]##
  33. ## Procs like `styledWriteLine`, `styledEcho` etc. have a temporary effect on
  34. ## text parameters. Style parameters only affect the text parameter right after them.
  35. ## After being called, these procs will reset the default style of the terminal.
  36. ## While `setBackGroundColor`, `setForeGroundColor` etc. have a lasting
  37. ## influence on the terminal, you can use `resetAttributes` to
  38. ## reset the default style of the terminal.
  39. runnableExamples("-r:off"):
  40. stdout.styledWriteLine({styleBright, styleBlink, styleUnderscore}, "styled text ")
  41. stdout.styledWriteLine(fgRed, "red text ")
  42. stdout.styledWriteLine(fgWhite, bgRed, "white text in red background")
  43. stdout.styledWriteLine(" ordinary text without style ")
  44. stdout.setBackGroundColor(bgCyan, true)
  45. stdout.setForeGroundColor(fgBlue)
  46. stdout.write("blue text in cyan background")
  47. stdout.resetAttributes()
  48. # You can specify multiple text parameters. Style parameters
  49. # only affect the text parameter right after them.
  50. styledEcho styleBright, fgGreen, "[PASS]", resetStyle, fgGreen, " Yay!"
  51. stdout.styledWriteLine(fgRed, "red text ", styleBright, "bold red", fgDefault, " bold text")
  52. import macros
  53. import strformat
  54. from strutils import toLowerAscii, `%`, parseInt
  55. import colors
  56. when defined(windows):
  57. import winlean
  58. when defined(nimPreviewSlimSystem):
  59. import std/[syncio, assertions]
  60. type
  61. PTerminal = ref object
  62. trueColorIsSupported: bool
  63. trueColorIsEnabled: bool
  64. fgSetColor: bool
  65. when defined(windows):
  66. hStdout: Handle
  67. hStderr: Handle
  68. oldStdoutAttr: int16
  69. oldStderrAttr: int16
  70. var gTerm {.threadvar.}: owned(PTerminal)
  71. when defined(windows) and defined(consoleapp):
  72. proc newTerminal(): owned(PTerminal) {.gcsafe, raises: [OSError].}
  73. else:
  74. proc newTerminal(): owned(PTerminal) {.gcsafe, raises: [].}
  75. proc getTerminal(): PTerminal {.inline.} =
  76. if isNil(gTerm):
  77. gTerm = newTerminal()
  78. result = gTerm
  79. const
  80. fgPrefix = "\e[38;2;"
  81. bgPrefix = "\e[48;2;"
  82. ansiResetCode* = "\e[0m"
  83. getPos = "\e[6n"
  84. stylePrefix = "\e["
  85. when defined(windows):
  86. import winlean, os
  87. const
  88. DUPLICATE_SAME_ACCESS = 2
  89. FOREGROUND_BLUE = 1
  90. FOREGROUND_GREEN = 2
  91. FOREGROUND_RED = 4
  92. FOREGROUND_INTENSITY = 8
  93. BACKGROUND_BLUE = 16
  94. BACKGROUND_GREEN = 32
  95. BACKGROUND_RED = 64
  96. BACKGROUND_INTENSITY = 128
  97. FOREGROUND_RGB = FOREGROUND_RED or FOREGROUND_GREEN or FOREGROUND_BLUE
  98. BACKGROUND_RGB = BACKGROUND_RED or BACKGROUND_GREEN or BACKGROUND_BLUE
  99. ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004
  100. type
  101. SHORT = int16
  102. COORD = object
  103. x: SHORT
  104. y: SHORT
  105. SMALL_RECT = object
  106. left: SHORT
  107. top: SHORT
  108. right: SHORT
  109. bottom: SHORT
  110. CONSOLE_SCREEN_BUFFER_INFO = object
  111. dwSize: COORD
  112. dwCursorPosition: COORD
  113. wAttributes: int16
  114. srWindow: SMALL_RECT
  115. dwMaximumWindowSize: COORD
  116. CONSOLE_CURSOR_INFO = object
  117. dwSize: DWORD
  118. bVisible: WINBOOL
  119. proc duplicateHandle(hSourceProcessHandle: Handle, hSourceHandle: Handle,
  120. hTargetProcessHandle: Handle, lpTargetHandle: ptr Handle,
  121. dwDesiredAccess: DWORD, bInheritHandle: WINBOOL,
  122. dwOptions: DWORD): WINBOOL{.stdcall, dynlib: "kernel32",
  123. importc: "DuplicateHandle".}
  124. proc getCurrentProcess(): Handle{.stdcall, dynlib: "kernel32",
  125. importc: "GetCurrentProcess".}
  126. proc getConsoleScreenBufferInfo(hConsoleOutput: Handle,
  127. lpConsoleScreenBufferInfo: ptr CONSOLE_SCREEN_BUFFER_INFO): WINBOOL{.stdcall,
  128. dynlib: "kernel32", importc: "GetConsoleScreenBufferInfo".}
  129. proc getConsoleCursorInfo(hConsoleOutput: Handle,
  130. lpConsoleCursorInfo: ptr CONSOLE_CURSOR_INFO): WINBOOL{.
  131. stdcall, dynlib: "kernel32", importc: "GetConsoleCursorInfo".}
  132. proc setConsoleCursorInfo(hConsoleOutput: Handle,
  133. lpConsoleCursorInfo: ptr CONSOLE_CURSOR_INFO): WINBOOL{.
  134. stdcall, dynlib: "kernel32", importc: "SetConsoleCursorInfo".}
  135. proc terminalWidthIoctl*(handles: openArray[Handle]): int =
  136. var csbi: CONSOLE_SCREEN_BUFFER_INFO
  137. for h in handles:
  138. if getConsoleScreenBufferInfo(h, addr csbi) != 0:
  139. return int(csbi.srWindow.right - csbi.srWindow.left + 1)
  140. return 0
  141. proc terminalHeightIoctl*(handles: openArray[Handle]): int =
  142. var csbi: CONSOLE_SCREEN_BUFFER_INFO
  143. for h in handles:
  144. if getConsoleScreenBufferInfo(h, addr csbi) != 0:
  145. return int(csbi.srWindow.bottom - csbi.srWindow.top + 1)
  146. return 0
  147. proc terminalWidth*(): int =
  148. ## Returns the terminal width in columns.
  149. var w: int = 0
  150. w = terminalWidthIoctl([getStdHandle(STD_INPUT_HANDLE),
  151. getStdHandle(STD_OUTPUT_HANDLE),
  152. getStdHandle(STD_ERROR_HANDLE)])
  153. if w > 0: return w
  154. return 80
  155. proc terminalHeight*(): int =
  156. ## Returns the terminal height in rows.
  157. var h: int = 0
  158. h = terminalHeightIoctl([getStdHandle(STD_INPUT_HANDLE),
  159. getStdHandle(STD_OUTPUT_HANDLE),
  160. getStdHandle(STD_ERROR_HANDLE)])
  161. if h > 0: return h
  162. return 0
  163. proc setConsoleCursorPosition(hConsoleOutput: Handle,
  164. dwCursorPosition: COORD): WINBOOL{.
  165. stdcall, dynlib: "kernel32", importc: "SetConsoleCursorPosition".}
  166. proc fillConsoleOutputCharacter(hConsoleOutput: Handle, cCharacter: char,
  167. nLength: DWORD, dwWriteCoord: COORD,
  168. lpNumberOfCharsWritten: ptr DWORD): WINBOOL{.
  169. stdcall, dynlib: "kernel32", importc: "FillConsoleOutputCharacterA".}
  170. proc fillConsoleOutputAttribute(hConsoleOutput: Handle, wAttribute: int16,
  171. nLength: DWORD, dwWriteCoord: COORD,
  172. lpNumberOfAttrsWritten: ptr DWORD): WINBOOL{.
  173. stdcall, dynlib: "kernel32", importc: "FillConsoleOutputAttribute".}
  174. proc setConsoleTextAttribute(hConsoleOutput: Handle,
  175. wAttributes: int16): WINBOOL{.
  176. stdcall, dynlib: "kernel32", importc: "SetConsoleTextAttribute".}
  177. proc getConsoleMode(hConsoleHandle: Handle, dwMode: ptr DWORD): WINBOOL{.
  178. stdcall, dynlib: "kernel32", importc: "GetConsoleMode".}
  179. proc setConsoleMode(hConsoleHandle: Handle, dwMode: DWORD): WINBOOL{.
  180. stdcall, dynlib: "kernel32", importc: "SetConsoleMode".}
  181. proc getCursorPos(h: Handle): tuple [x, y: int] =
  182. var c: CONSOLE_SCREEN_BUFFER_INFO
  183. if getConsoleScreenBufferInfo(h, addr(c)) == 0:
  184. raiseOSError(osLastError())
  185. return (int(c.dwCursorPosition.x), int(c.dwCursorPosition.y))
  186. proc getCursorPos*(): tuple [x, y: int] {.raises: [ValueError, IOError, OSError].} =
  187. return getCursorPos(getStdHandle(STD_OUTPUT_HANDLE))
  188. proc setCursorPos(h: Handle, x, y: int) =
  189. var c: COORD
  190. c.x = int16(x)
  191. c.y = int16(y)
  192. if setConsoleCursorPosition(h, c) == 0:
  193. raiseOSError(osLastError())
  194. proc getAttributes(h: Handle): int16 =
  195. var c: CONSOLE_SCREEN_BUFFER_INFO
  196. # workaround Windows bugs: try several times
  197. if getConsoleScreenBufferInfo(h, addr(c)) != 0:
  198. return c.wAttributes
  199. return 0x70'i16 # ERROR: return white background, black text
  200. proc initTerminal(term: PTerminal) =
  201. var hStdoutTemp = getStdHandle(STD_OUTPUT_HANDLE)
  202. if duplicateHandle(getCurrentProcess(), hStdoutTemp, getCurrentProcess(),
  203. addr(term.hStdout), 0, 1, DUPLICATE_SAME_ACCESS) == 0:
  204. when defined(consoleapp):
  205. raiseOSError(osLastError())
  206. var hStderrTemp = getStdHandle(STD_ERROR_HANDLE)
  207. if duplicateHandle(getCurrentProcess(), hStderrTemp, getCurrentProcess(),
  208. addr(term.hStderr), 0, 1, DUPLICATE_SAME_ACCESS) == 0:
  209. when defined(consoleapp):
  210. raiseOSError(osLastError())
  211. term.oldStdoutAttr = getAttributes(term.hStdout)
  212. term.oldStderrAttr = getAttributes(term.hStderr)
  213. template conHandle(f: File): Handle =
  214. let term = getTerminal()
  215. if f == stderr: term.hStderr else: term.hStdout
  216. else:
  217. import termios, posix, os, parseutils
  218. proc setRaw(fd: FileHandle, time: cint = TCSAFLUSH) =
  219. var mode: Termios
  220. discard fd.tcGetAttr(addr mode)
  221. mode.c_iflag = mode.c_iflag and not Cflag(BRKINT or ICRNL or INPCK or
  222. ISTRIP or IXON)
  223. mode.c_oflag = mode.c_oflag and not Cflag(OPOST)
  224. mode.c_cflag = (mode.c_cflag and not Cflag(CSIZE or PARENB)) or CS8
  225. mode.c_lflag = mode.c_lflag and not Cflag(ECHO or ICANON or IEXTEN or ISIG)
  226. mode.c_cc[VMIN] = 1.cuchar
  227. mode.c_cc[VTIME] = 0.cuchar
  228. discard fd.tcSetAttr(time, addr mode)
  229. proc getCursorPos*(): tuple [x, y: int] {.raises: [ValueError, IOError].} =
  230. ## Returns cursor position (x, y)
  231. ## writes to stdout and expects the terminal to respond via stdin
  232. var
  233. xStr = ""
  234. yStr = ""
  235. ch: char
  236. ct: int
  237. readX = false
  238. # use raw mode to ask terminal for cursor position
  239. let fd = getFileHandle(stdin)
  240. var oldMode: Termios
  241. discard fd.tcGetAttr(addr oldMode)
  242. fd.setRaw()
  243. stdout.write(getPos)
  244. flushFile(stdout)
  245. try:
  246. # parse response format: [yyy;xxxR
  247. while true:
  248. let n = readBuffer(stdin, addr ch, 1)
  249. if n == 0 or ch == 'R':
  250. if xStr == "" or yStr == "":
  251. raise newException(ValueError, "Got character position message that was missing data")
  252. break
  253. ct += 1
  254. if ct > 16:
  255. raise newException(ValueError, "Got unterminated character position message from terminal")
  256. if ch == ';':
  257. readX = true
  258. elif ch in {'0'..'9'}:
  259. if readX:
  260. xStr.add(ch)
  261. else:
  262. yStr.add(ch)
  263. finally:
  264. # restore previous terminal mode
  265. discard fd.tcSetAttr(TCSADRAIN, addr oldMode)
  266. return (parseInt(xStr), parseInt(yStr))
  267. proc terminalWidthIoctl*(fds: openArray[int]): int =
  268. ## Returns terminal width from first fd that supports the ioctl.
  269. var win: IOctl_WinSize
  270. for fd in fds:
  271. if ioctl(cint(fd), TIOCGWINSZ, addr win) != -1:
  272. return int(win.ws_col)
  273. return 0
  274. proc terminalHeightIoctl*(fds: openArray[int]): int =
  275. ## Returns terminal height from first fd that supports the ioctl.
  276. var win: IOctl_WinSize
  277. for fd in fds:
  278. if ioctl(cint(fd), TIOCGWINSZ, addr win) != -1:
  279. return int(win.ws_row)
  280. return 0
  281. var L_ctermid{.importc, header: "<stdio.h>".}: cint
  282. proc terminalWidth*(): int =
  283. ## Returns some reasonable terminal width from either standard file
  284. ## descriptors, controlling terminal, environment variables or tradition.
  285. # POSIX environment variable takes precendence.
  286. # _COLUMNS_: This variable shall represent a decimal integer >0 used
  287. # to indicate the user's preferred width in column positions for
  288. # the terminal screen or window. If this variable is unset or null,
  289. # the implementation determines the number of columns, appropriate
  290. # for the terminal or window, in an unspecified manner.
  291. # When COLUMNS is set, any terminal-width information implied by TERM
  292. # is overridden. Users and conforming applications should not set COLUMNS
  293. # unless they wish to override the system selection and produce output
  294. # unrelated to the terminal characteristics.
  295. # See POSIX Base Definitions Section 8.1 Environment Variable Definition
  296. var w: int
  297. var s = getEnv("COLUMNS") # Try standard env var
  298. if len(s) > 0 and parseSaturatedNatural(s, w) > 0 and w > 0:
  299. return w
  300. w = terminalWidthIoctl([0, 1, 2]) # Try standard file descriptors
  301. if w > 0: return w
  302. var cterm = newString(L_ctermid) # Try controlling tty
  303. var fd = open(ctermid(cstring(cterm)), O_RDONLY)
  304. if fd != -1:
  305. w = terminalWidthIoctl([int(fd)])
  306. discard close(fd)
  307. if w > 0: return w
  308. return 80 # Finally default to venerable value
  309. proc terminalHeight*(): int =
  310. ## Returns some reasonable terminal height from either standard file
  311. ## descriptors, controlling terminal, environment variables or tradition.
  312. ## Zero is returned if the height could not be determined.
  313. # POSIX environment variable takes precendence.
  314. # _LINES_: This variable shall represent a decimal integer >0 used
  315. # to indicate the user's preferred number of lines on a page or
  316. # the vertical screen or window size in lines. A line in this case
  317. # is a vertical measure large enough to hold the tallest character
  318. # in the character set being displayed. If this variable is unset or null,
  319. # the implementation determines the number of lines, appropriate
  320. # for the terminal or window (size, terminal baud rate, and so on),
  321. # in an unspecified manner.
  322. # When LINES is set, any terminal-height information implied by TERM
  323. # is overridden. Users and conforming applications should not set LINES
  324. # unless they wish to override the system selection and produce output
  325. # unrelated to the terminal characteristics.
  326. # See POSIX Base Definitions Section 8.1 Environment Variable Definition
  327. var h: int
  328. var s = getEnv("LINES") # Try standard env var
  329. if len(s) > 0 and parseSaturatedNatural(s, h) > 0 and h > 0:
  330. return h
  331. h = terminalHeightIoctl([0, 1, 2]) # Try standard file descriptors
  332. if h > 0: return h
  333. var cterm = newString(L_ctermid) # Try controlling tty
  334. var fd = open(ctermid(cstring(cterm)), O_RDONLY)
  335. if fd != -1:
  336. h = terminalHeightIoctl([int(fd)])
  337. discard close(fd)
  338. if h > 0: return h
  339. return 0 # Could not determine height
  340. proc terminalSize*(): tuple[w, h: int] =
  341. ## Returns the terminal width and height as a tuple. Internally calls
  342. ## `terminalWidth` and `terminalHeight`, so the same assumptions apply.
  343. result = (terminalWidth(), terminalHeight())
  344. when defined(windows):
  345. proc setCursorVisibility(f: File, visible: bool) =
  346. var ccsi: CONSOLE_CURSOR_INFO
  347. let h = conHandle(f)
  348. if getConsoleCursorInfo(h, addr(ccsi)) == 0:
  349. raiseOSError(osLastError())
  350. ccsi.bVisible = if visible: 1 else: 0
  351. if setConsoleCursorInfo(h, addr(ccsi)) == 0:
  352. raiseOSError(osLastError())
  353. proc hideCursor*(f: File) =
  354. ## Hides the cursor.
  355. when defined(windows):
  356. setCursorVisibility(f, false)
  357. else:
  358. f.write("\e[?25l")
  359. proc showCursor*(f: File) =
  360. ## Shows the cursor.
  361. when defined(windows):
  362. setCursorVisibility(f, true)
  363. else:
  364. f.write("\e[?25h")
  365. proc setCursorPos*(f: File, x, y: int) =
  366. ## Sets the terminal's cursor to the (x,y) position.
  367. ## (0,0) is the upper left of the screen.
  368. when defined(windows):
  369. let h = conHandle(f)
  370. setCursorPos(h, x, y)
  371. else:
  372. f.write(fmt"{stylePrefix}{y+1};{x+1}f")
  373. proc setCursorXPos*(f: File, x: int) =
  374. ## Sets the terminal's cursor to the x position.
  375. ## The y position is not changed.
  376. when defined(windows):
  377. let h = conHandle(f)
  378. var scrbuf: CONSOLE_SCREEN_BUFFER_INFO
  379. if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0:
  380. raiseOSError(osLastError())
  381. var origin = scrbuf.dwCursorPosition
  382. origin.x = int16(x)
  383. if setConsoleCursorPosition(h, origin) == 0:
  384. raiseOSError(osLastError())
  385. else:
  386. f.write(fmt"{stylePrefix}{x+1}G")
  387. when defined(windows):
  388. proc setCursorYPos*(f: File, y: int) =
  389. ## Sets the terminal's cursor to the y position.
  390. ## The x position is not changed.
  391. ## .. warning:: This is not supported on UNIX!
  392. when defined(windows):
  393. let h = conHandle(f)
  394. var scrbuf: CONSOLE_SCREEN_BUFFER_INFO
  395. if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0:
  396. raiseOSError(osLastError())
  397. var origin = scrbuf.dwCursorPosition
  398. origin.y = int16(y)
  399. if setConsoleCursorPosition(h, origin) == 0:
  400. raiseOSError(osLastError())
  401. else:
  402. discard
  403. proc cursorUp*(f: File, count = 1) =
  404. ## Moves the cursor up by `count` rows.
  405. runnableExamples("-r:off"):
  406. stdout.cursorUp(2)
  407. write(stdout, "Hello World!") # anything written at that location will be erased/replaced with this
  408. when defined(windows):
  409. let h = conHandle(f)
  410. var p = getCursorPos(h)
  411. dec(p.y, count)
  412. setCursorPos(h, p.x, p.y)
  413. else:
  414. f.write("\e[" & $count & 'A')
  415. proc cursorDown*(f: File, count = 1) =
  416. ## Moves the cursor down by `count` rows.
  417. runnableExamples("-r:off"):
  418. stdout.cursorDown(2)
  419. write(stdout, "Hello World!") # anything written at that location will be erased/replaced with this
  420. when defined(windows):
  421. let h = conHandle(f)
  422. var p = getCursorPos(h)
  423. inc(p.y, count)
  424. setCursorPos(h, p.x, p.y)
  425. else:
  426. f.write(fmt"{stylePrefix}{count}B")
  427. proc cursorForward*(f: File, count = 1) =
  428. ## Moves the cursor forward by `count` columns.
  429. runnableExamples("-r:off"):
  430. stdout.cursorForward(2)
  431. write(stdout, "Hello World!") # anything written at that location will be erased/replaced with this
  432. when defined(windows):
  433. let h = conHandle(f)
  434. var p = getCursorPos(h)
  435. inc(p.x, count)
  436. setCursorPos(h, p.x, p.y)
  437. else:
  438. f.write(fmt"{stylePrefix}{count}C")
  439. proc cursorBackward*(f: File, count = 1) =
  440. ## Moves the cursor backward by `count` columns.
  441. runnableExamples("-r:off"):
  442. stdout.cursorBackward(2)
  443. write(stdout, "Hello World!") # anything written at that location will be erased/replaced with this
  444. when defined(windows):
  445. let h = conHandle(f)
  446. var p = getCursorPos(h)
  447. dec(p.x, count)
  448. setCursorPos(h, p.x, p.y)
  449. else:
  450. f.write(fmt"{stylePrefix}{count}D")
  451. when true:
  452. discard
  453. else:
  454. proc eraseLineEnd*(f: File) =
  455. ## Erases from the current cursor position to the end of the current line.
  456. when defined(windows):
  457. discard
  458. else:
  459. f.write("\e[K")
  460. proc eraseLineStart*(f: File) =
  461. ## Erases from the current cursor position to the start of the current line.
  462. when defined(windows):
  463. discard
  464. else:
  465. f.write("\e[1K")
  466. proc eraseDown*(f: File) =
  467. ## Erases the screen from the current line down to the bottom of the screen.
  468. when defined(windows):
  469. discard
  470. else:
  471. f.write("\e[J")
  472. proc eraseUp*(f: File) =
  473. ## Erases the screen from the current line up to the top of the screen.
  474. when defined(windows):
  475. discard
  476. else:
  477. f.write("\e[1J")
  478. proc eraseLine*(f: File) =
  479. ## Erases the entire current line.
  480. runnableExamples("-r:off"):
  481. write(stdout, "never mind")
  482. stdout.eraseLine() # nothing will be printed on the screen
  483. when defined(windows):
  484. let h = conHandle(f)
  485. var scrbuf: CONSOLE_SCREEN_BUFFER_INFO
  486. var numwrote: DWORD
  487. if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0:
  488. raiseOSError(osLastError())
  489. var origin = scrbuf.dwCursorPosition
  490. origin.x = 0'i16
  491. if setConsoleCursorPosition(h, origin) == 0:
  492. raiseOSError(osLastError())
  493. var wt: DWORD = scrbuf.dwSize.x - origin.x
  494. if fillConsoleOutputCharacter(h, ' ', wt,
  495. origin, addr(numwrote)) == 0:
  496. raiseOSError(osLastError())
  497. if fillConsoleOutputAttribute(h, scrbuf.wAttributes, wt,
  498. scrbuf.dwCursorPosition, addr(numwrote)) == 0:
  499. raiseOSError(osLastError())
  500. else:
  501. f.write("\e[2K")
  502. setCursorXPos(f, 0)
  503. proc eraseScreen*(f: File) =
  504. ## Erases the screen with the background colour and moves the cursor to home.
  505. when defined(windows):
  506. let h = conHandle(f)
  507. var scrbuf: CONSOLE_SCREEN_BUFFER_INFO
  508. var numwrote: DWORD
  509. var origin: COORD # is inititalized to 0, 0
  510. if getConsoleScreenBufferInfo(h, addr(scrbuf)) == 0:
  511. raiseOSError(osLastError())
  512. let numChars = int32(scrbuf.dwSize.x)*int32(scrbuf.dwSize.y)
  513. if fillConsoleOutputCharacter(h, ' ', numChars,
  514. origin, addr(numwrote)) == 0:
  515. raiseOSError(osLastError())
  516. if fillConsoleOutputAttribute(h, scrbuf.wAttributes, numChars,
  517. origin, addr(numwrote)) == 0:
  518. raiseOSError(osLastError())
  519. setCursorXPos(f, 0)
  520. else:
  521. f.write("\e[2J")
  522. when not defined(windows):
  523. var
  524. gFG {.threadvar.}: int
  525. gBG {.threadvar.}: int
  526. proc resetAttributes*(f: File) =
  527. ## Resets all attributes.
  528. when defined(windows):
  529. let term = getTerminal()
  530. if f == stderr:
  531. discard setConsoleTextAttribute(term.hStderr, term.oldStderrAttr)
  532. else:
  533. discard setConsoleTextAttribute(term.hStdout, term.oldStdoutAttr)
  534. else:
  535. f.write(ansiResetCode)
  536. gFG = 0
  537. gBG = 0
  538. type
  539. Style* = enum ## Different styles for text output.
  540. styleBright = 1, ## bright text
  541. styleDim, ## dim text
  542. styleItalic, ## italic (or reverse on terminals not supporting)
  543. styleUnderscore, ## underscored text
  544. styleBlink, ## blinking/bold text
  545. styleBlinkRapid, ## rapid blinking/bold text (not widely supported)
  546. styleReverse, ## reverse
  547. styleHidden, ## hidden text
  548. styleStrikethrough ## strikethrough
  549. proc ansiStyleCode*(style: int): string =
  550. result = fmt"{stylePrefix}{style}m"
  551. template ansiStyleCode*(style: Style): string =
  552. ansiStyleCode(style.int)
  553. # The styleCache can be skipped when `style` is known at compile-time
  554. template ansiStyleCode*(style: static[Style]): string =
  555. (static(stylePrefix & $style.int & "m"))
  556. proc setStyle*(f: File, style: set[Style]) =
  557. ## Sets the terminal style.
  558. when defined(windows):
  559. let h = conHandle(f)
  560. var old = getAttributes(h) and (FOREGROUND_RGB or BACKGROUND_RGB)
  561. var a = 0'i16
  562. if styleBright in style: a = a or int16(FOREGROUND_INTENSITY)
  563. if styleBlink in style: a = a or int16(BACKGROUND_INTENSITY)
  564. if styleReverse in style: a = a or 0x4000'i16 # COMMON_LVB_REVERSE_VIDEO
  565. if styleUnderscore in style: a = a or 0x8000'i16 # COMMON_LVB_UNDERSCORE
  566. discard setConsoleTextAttribute(h, old or a)
  567. else:
  568. for s in items(style):
  569. f.write(ansiStyleCode(s))
  570. proc writeStyled*(txt: string, style: set[Style] = {styleBright}) =
  571. ## Writes the text `txt` in a given `style` to stdout.
  572. when defined(windows):
  573. let term = getTerminal()
  574. var old = getAttributes(term.hStdout)
  575. stdout.setStyle(style)
  576. stdout.write(txt)
  577. discard setConsoleTextAttribute(term.hStdout, old)
  578. else:
  579. stdout.setStyle(style)
  580. stdout.write(txt)
  581. stdout.resetAttributes()
  582. if gFG != 0:
  583. stdout.write(ansiStyleCode(gFG))
  584. if gBG != 0:
  585. stdout.write(ansiStyleCode(gBG))
  586. type
  587. ForegroundColor* = enum ## Terminal's foreground colors.
  588. fgBlack = 30, ## black
  589. fgRed, ## red
  590. fgGreen, ## green
  591. fgYellow, ## yellow
  592. fgBlue, ## blue
  593. fgMagenta, ## magenta
  594. fgCyan, ## cyan
  595. fgWhite, ## white
  596. fg8Bit, ## 256-color (not supported, see `enableTrueColors` instead.)
  597. fgDefault ## default terminal foreground color
  598. BackgroundColor* = enum ## Terminal's background colors.
  599. bgBlack = 40, ## black
  600. bgRed, ## red
  601. bgGreen, ## green
  602. bgYellow, ## yellow
  603. bgBlue, ## blue
  604. bgMagenta, ## magenta
  605. bgCyan, ## cyan
  606. bgWhite, ## white
  607. bg8Bit, ## 256-color (not supported, see `enableTrueColors` instead.)
  608. bgDefault ## default terminal background color
  609. when defined(windows):
  610. var defaultForegroundColor, defaultBackgroundColor: int16 = 0xFFFF'i16 # Default to an invalid value 0xFFFF
  611. proc setForegroundColor*(f: File, fg: ForegroundColor, bright = false) =
  612. ## Sets the terminal's foreground color.
  613. when defined(windows):
  614. let h = conHandle(f)
  615. var old = getAttributes(h) and not FOREGROUND_RGB
  616. if defaultForegroundColor == 0xFFFF'i16:
  617. defaultForegroundColor = old
  618. old = if bright: old or FOREGROUND_INTENSITY
  619. else: old and not(FOREGROUND_INTENSITY)
  620. const lookup: array[ForegroundColor, int] = [
  621. 0, # ForegroundColor enum with ordinal 30
  622. (FOREGROUND_RED),
  623. (FOREGROUND_GREEN),
  624. (FOREGROUND_RED or FOREGROUND_GREEN),
  625. (FOREGROUND_BLUE),
  626. (FOREGROUND_RED or FOREGROUND_BLUE),
  627. (FOREGROUND_BLUE or FOREGROUND_GREEN),
  628. (FOREGROUND_BLUE or FOREGROUND_GREEN or FOREGROUND_RED),
  629. 0, # fg8Bit not supported, see `enableTrueColors` instead.
  630. 0] # unused
  631. if fg == fgDefault:
  632. discard setConsoleTextAttribute(h, cast[int16](cast[uint16](old) or cast[uint16](defaultForegroundColor)))
  633. else:
  634. discard setConsoleTextAttribute(h, cast[int16](cast[uint16](old) or cast[uint16](lookup[fg])))
  635. else:
  636. gFG = ord(fg)
  637. if bright: inc(gFG, 60)
  638. f.write(ansiStyleCode(gFG))
  639. proc setBackgroundColor*(f: File, bg: BackgroundColor, bright = false) =
  640. ## Sets the terminal's background color.
  641. when defined(windows):
  642. let h = conHandle(f)
  643. var old = getAttributes(h) and not BACKGROUND_RGB
  644. if defaultBackgroundColor == 0xFFFF'i16:
  645. defaultBackgroundColor = old
  646. old = if bright: old or BACKGROUND_INTENSITY
  647. else: old and not(BACKGROUND_INTENSITY)
  648. const lookup: array[BackgroundColor, int] = [
  649. 0, # BackgroundColor enum with ordinal 40
  650. (BACKGROUND_RED),
  651. (BACKGROUND_GREEN),
  652. (BACKGROUND_RED or BACKGROUND_GREEN),
  653. (BACKGROUND_BLUE),
  654. (BACKGROUND_RED or BACKGROUND_BLUE),
  655. (BACKGROUND_BLUE or BACKGROUND_GREEN),
  656. (BACKGROUND_BLUE or BACKGROUND_GREEN or BACKGROUND_RED),
  657. 0, # bg8Bit not supported, see `enableTrueColors` instead.
  658. 0] # unused
  659. if bg == bgDefault:
  660. discard setConsoleTextAttribute(h, cast[int16](cast[uint16](old) or cast[uint16](defaultBackgroundColor)))
  661. else:
  662. discard setConsoleTextAttribute(h, cast[int16](cast[uint16](old) or cast[uint16](lookup[bg])))
  663. else:
  664. gBG = ord(bg)
  665. if bright: inc(gBG, 60)
  666. f.write(ansiStyleCode(gBG))
  667. proc ansiForegroundColorCode*(fg: ForegroundColor, bright = false): string =
  668. var style = ord(fg)
  669. if bright: inc(style, 60)
  670. return ansiStyleCode(style)
  671. template ansiForegroundColorCode*(fg: static[ForegroundColor],
  672. bright: static[bool] = false): string =
  673. ansiStyleCode(fg.int + bright.int * 60)
  674. proc ansiForegroundColorCode*(color: Color): string =
  675. let rgb = extractRGB(color)
  676. result = fmt"{fgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
  677. template ansiForegroundColorCode*(color: static[Color]): string =
  678. const rgb = extractRGB(color)
  679. # no usage of `fmt`, see issue #7632
  680. (static("$1$2;$3;$4m" % [$fgPrefix, $(rgb.r), $(rgb.g), $(rgb.b)]))
  681. proc ansiBackgroundColorCode*(color: Color): string =
  682. let rgb = extractRGB(color)
  683. result = fmt"{bgPrefix}{rgb.r};{rgb.g};{rgb.b}m"
  684. template ansiBackgroundColorCode*(color: static[Color]): string =
  685. const rgb = extractRGB(color)
  686. # no usage of `fmt`, see issue #7632
  687. (static("$1$2;$3;$4m" % [$bgPrefix, $(rgb.r), $(rgb.g), $(rgb.b)]))
  688. proc setForegroundColor*(f: File, color: Color) =
  689. ## Sets the terminal's foreground true color.
  690. if getTerminal().trueColorIsEnabled:
  691. f.write(ansiForegroundColorCode(color))
  692. proc setBackgroundColor*(f: File, color: Color) =
  693. ## Sets the terminal's background true color.
  694. if getTerminal().trueColorIsEnabled:
  695. f.write(ansiBackgroundColorCode(color))
  696. proc setTrueColor(f: File, color: Color) =
  697. let term = getTerminal()
  698. if term.fgSetColor:
  699. setForegroundColor(f, color)
  700. else:
  701. setBackgroundColor(f, color)
  702. proc isatty*(f: File): bool =
  703. ## Returns true if `f` is associated with a terminal device.
  704. when defined(posix):
  705. proc isatty(fildes: FileHandle): cint {.
  706. importc: "isatty", header: "<unistd.h>".}
  707. else:
  708. proc isatty(fildes: FileHandle): cint {.
  709. importc: "_isatty", header: "<io.h>".}
  710. result = isatty(getFileHandle(f)) != 0'i32
  711. type
  712. TerminalCmd* = enum ## commands that can be expressed as arguments
  713. resetStyle, ## reset attributes
  714. fgColor, ## set foreground's true color
  715. bgColor ## set background's true color
  716. template styledEchoProcessArg(f: File, s: string) = write f, s
  717. template styledEchoProcessArg(f: File, style: Style) = setStyle(f, {style})
  718. template styledEchoProcessArg(f: File, style: set[Style]) = setStyle f, style
  719. template styledEchoProcessArg(f: File, color: ForegroundColor) =
  720. setForegroundColor f, color
  721. template styledEchoProcessArg(f: File, color: BackgroundColor) =
  722. setBackgroundColor f, color
  723. template styledEchoProcessArg(f: File, color: Color) =
  724. setTrueColor f, color
  725. template styledEchoProcessArg(f: File, cmd: TerminalCmd) =
  726. when cmd == resetStyle:
  727. resetAttributes(f)
  728. elif cmd in {fgColor, bgColor}:
  729. let term = getTerminal()
  730. term.fgSetColor = cmd == fgColor
  731. macro styledWrite*(f: File, m: varargs[typed]): untyped =
  732. ## Similar to `write`, but treating terminal style arguments specially.
  733. ## When some argument is `Style`, `set[Style]`, `ForegroundColor`,
  734. ## `BackgroundColor` or `TerminalCmd` then it is not sent directly to
  735. ## `f`, but instead corresponding terminal style proc is called.
  736. runnableExamples("-r:off"):
  737. stdout.styledWrite(fgRed, "red text ")
  738. stdout.styledWrite(fgGreen, "green text")
  739. var reset = false
  740. result = newNimNode(nnkStmtList)
  741. for i in countup(0, m.len - 1):
  742. let item = m[i]
  743. case item.kind
  744. of nnkStrLit..nnkTripleStrLit:
  745. if i == m.len - 1:
  746. # optimize if string literal is last, just call write
  747. result.add(newCall(bindSym"write", f, item))
  748. if reset: result.add(newCall(bindSym"resetAttributes", f))
  749. return
  750. else:
  751. # if it is string literal just call write, do not enable reset
  752. result.add(newCall(bindSym"write", f, item))
  753. else:
  754. result.add(newCall(bindSym"styledEchoProcessArg", f, item))
  755. reset = true
  756. if reset: result.add(newCall(bindSym"resetAttributes", f))
  757. template styledWriteLine*(f: File, args: varargs[untyped]) =
  758. ## Calls `styledWrite` and appends a newline at the end.
  759. runnableExamples:
  760. proc error(msg: string) =
  761. styledWriteLine(stderr, fgRed, "Error: ", resetStyle, msg)
  762. styledWrite(f, args)
  763. write(f, "\n")
  764. template styledEcho*(args: varargs[untyped]) =
  765. ## Echoes styles arguments to stdout using `styledWriteLine`.
  766. stdout.styledWriteLine(args)
  767. proc getch*(): char =
  768. ## Reads a single character from the terminal, blocking until it is entered.
  769. ## The character is not printed to the terminal.
  770. when defined(windows):
  771. let fd = getStdHandle(STD_INPUT_HANDLE)
  772. var keyEvent = KEY_EVENT_RECORD()
  773. var numRead: cint
  774. while true:
  775. # Block until character is entered
  776. doAssert(waitForSingleObject(fd, INFINITE) == WAIT_OBJECT_0)
  777. doAssert(readConsoleInput(fd, addr(keyEvent), 1, addr(numRead)) != 0)
  778. if numRead == 0 or keyEvent.eventType != 1 or keyEvent.bKeyDown == 0:
  779. continue
  780. return char(keyEvent.uChar)
  781. else:
  782. let fd = getFileHandle(stdin)
  783. var oldMode: Termios
  784. discard fd.tcGetAttr(addr oldMode)
  785. fd.setRaw()
  786. result = stdin.readChar()
  787. discard fd.tcSetAttr(TCSADRAIN, addr oldMode)
  788. when defined(windows):
  789. proc readPasswordFromStdin*(prompt: string, password: var string):
  790. bool {.tags: [ReadIOEffect, WriteIOEffect].} =
  791. ## Reads a `password` from stdin without printing it. `password` must not
  792. ## be `nil`! Returns `false` if the end of the file has been reached,
  793. ## `true` otherwise.
  794. password.setLen(0)
  795. stdout.write(prompt)
  796. let hi = createFileA("CONIN$",
  797. GENERIC_READ or GENERIC_WRITE, 0, nil, OPEN_EXISTING, 0, 0)
  798. var mode = DWORD 0
  799. discard getConsoleMode(hi, addr mode)
  800. let origMode = mode
  801. const
  802. ENABLE_PROCESSED_INPUT = 1
  803. ENABLE_ECHO_INPUT = 4
  804. mode = (mode or ENABLE_PROCESSED_INPUT) and not ENABLE_ECHO_INPUT
  805. discard setConsoleMode(hi, mode)
  806. result = readLine(stdin, password)
  807. discard setConsoleMode(hi, origMode)
  808. discard closeHandle(hi)
  809. stdout.write "\n"
  810. else:
  811. import termios
  812. proc readPasswordFromStdin*(prompt: string, password: var string):
  813. bool {.tags: [ReadIOEffect, WriteIOEffect].} =
  814. password.setLen(0)
  815. let fd = stdin.getFileHandle()
  816. var cur, old: Termios
  817. discard fd.tcGetAttr(cur.addr)
  818. old = cur
  819. cur.c_lflag = cur.c_lflag and not Cflag(ECHO)
  820. discard fd.tcSetAttr(TCSADRAIN, cur.addr)
  821. stdout.write prompt
  822. result = stdin.readLine(password)
  823. stdout.write "\n"
  824. discard fd.tcSetAttr(TCSADRAIN, old.addr)
  825. proc readPasswordFromStdin*(prompt = "password: "): string =
  826. ## Reads a password from stdin without printing it.
  827. result = ""
  828. discard readPasswordFromStdin(prompt, result)
  829. # Wrappers assuming output to stdout:
  830. template hideCursor*() = hideCursor(stdout)
  831. template showCursor*() = showCursor(stdout)
  832. template setCursorPos*(x, y: int) = setCursorPos(stdout, x, y)
  833. template setCursorXPos*(x: int) = setCursorXPos(stdout, x)
  834. when defined(windows):
  835. template setCursorYPos*(x: int) = setCursorYPos(stdout, x)
  836. template cursorUp*(count = 1) = cursorUp(stdout, count)
  837. template cursorDown*(count = 1) = cursorDown(stdout, count)
  838. template cursorForward*(count = 1) = cursorForward(stdout, count)
  839. template cursorBackward*(count = 1) = cursorBackward(stdout, count)
  840. template eraseLine*() = eraseLine(stdout)
  841. template eraseScreen*() = eraseScreen(stdout)
  842. template setStyle*(style: set[Style]) =
  843. setStyle(stdout, style)
  844. template setForegroundColor*(fg: ForegroundColor, bright = false) =
  845. setForegroundColor(stdout, fg, bright)
  846. template setBackgroundColor*(bg: BackgroundColor, bright = false) =
  847. setBackgroundColor(stdout, bg, bright)
  848. template setForegroundColor*(color: Color) =
  849. setForegroundColor(stdout, color)
  850. template setBackgroundColor*(color: Color) =
  851. setBackgroundColor(stdout, color)
  852. proc resetAttributes*() {.noconv.} =
  853. ## Resets all attributes on stdout.
  854. ## It is advisable to register this as a quit proc with
  855. ## `exitprocs.addExitProc(resetAttributes)`.
  856. resetAttributes(stdout)
  857. proc isTrueColorSupported*(): bool =
  858. ## Returns true if a terminal supports true color.
  859. return getTerminal().trueColorIsSupported
  860. when defined(windows):
  861. import os
  862. proc enableTrueColors*() =
  863. ## Enables true color.
  864. var term = getTerminal()
  865. when defined(windows):
  866. var
  867. ver: OSVERSIONINFO
  868. ver.dwOSVersionInfoSize = sizeof(ver).DWORD
  869. let res = getVersionExW(addr ver)
  870. if res == 0:
  871. term.trueColorIsSupported = false
  872. else:
  873. term.trueColorIsSupported = ver.dwMajorVersion > 10 or
  874. (ver.dwMajorVersion == 10 and (ver.dwMinorVersion > 0 or
  875. (ver.dwMinorVersion == 0 and ver.dwBuildNumber >= 10586)))
  876. if not term.trueColorIsSupported:
  877. term.trueColorIsSupported = getEnv("ANSICON_DEF").len > 0
  878. if term.trueColorIsSupported:
  879. if getEnv("ANSICON_DEF").len == 0:
  880. var mode: DWORD = 0
  881. if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), addr(mode)) != 0:
  882. mode = mode or ENABLE_VIRTUAL_TERMINAL_PROCESSING
  883. if setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode) != 0:
  884. term.trueColorIsEnabled = true
  885. else:
  886. term.trueColorIsEnabled = false
  887. else:
  888. term.trueColorIsEnabled = true
  889. else:
  890. term.trueColorIsSupported = getEnv("COLORTERM").toLowerAscii() in [
  891. "truecolor", "24bit"]
  892. term.trueColorIsEnabled = term.trueColorIsSupported
  893. proc disableTrueColors*() =
  894. ## Disables true color.
  895. var term = getTerminal()
  896. when defined(windows):
  897. if term.trueColorIsSupported:
  898. if getEnv("ANSICON_DEF").len == 0:
  899. var mode: DWORD = 0
  900. if getConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), addr(mode)) != 0:
  901. mode = mode and not ENABLE_VIRTUAL_TERMINAL_PROCESSING
  902. discard setConsoleMode(getStdHandle(STD_OUTPUT_HANDLE), mode)
  903. term.trueColorIsEnabled = false
  904. else:
  905. term.trueColorIsEnabled = false
  906. proc newTerminal(): owned(PTerminal) =
  907. new result
  908. when defined(windows):
  909. initTerminal(result)