terminal.nim 32 KB

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