terminal.nim 33 KB

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