screen.py 48 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378
  1. #!/usr/bin/env python
  2. # License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
  3. from kitty.config import defaults
  4. from kitty.fast_data_types import DECAWM, DECCOLM, DECOM, IRM, VT_PARSER_BUFFER_SIZE, Color, ColorProfile, Cursor
  5. from kitty.marks import marker_from_function, marker_from_regex
  6. from kitty.rgb import color_names
  7. from kitty.window import pagerhist
  8. from . import BaseTest, parse_bytes
  9. class TestScreen(BaseTest):
  10. def test_draw_fast(self):
  11. s = self.create_screen()
  12. # Test in line-wrap, non-insert mode
  13. s.draw('a' * 5)
  14. self.ae(str(s.line(0)), 'a' * 5)
  15. self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
  16. s.draw('b' * 7)
  17. self.assertTrue(s.linebuf.is_continued(1))
  18. self.assertTrue(s.linebuf.is_continued(2))
  19. self.ae(str(s.line(0)), 'a' * 5)
  20. self.ae(str(s.line(1)), 'b' * 5)
  21. self.ae(str(s.line(2)), 'b' * 2)
  22. self.ae(s.cursor.x, 2), self.ae(s.cursor.y, 2)
  23. s.draw('c' * 15)
  24. self.ae(str(s.line(0)), 'b' * 5)
  25. self.ae(str(s.line(1)), 'bbccc')
  26. # Now test without line-wrap
  27. s.reset(), s.reset_dirty()
  28. s.reset_mode(DECAWM)
  29. s.draw('0123456789')
  30. self.ae(str(s.line(0)), '01239')
  31. self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
  32. s.draw('ab')
  33. self.ae(str(s.line(0)), '0123b')
  34. self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
  35. # Now test in insert mode
  36. s.reset(), s.reset_dirty()
  37. s.set_mode(IRM)
  38. s.draw('12345' * 5)
  39. s.cursor_back(5)
  40. self.ae(s.cursor.x, 0), self.ae(s.cursor.y, 4)
  41. s.reset_dirty()
  42. s.draw('ab')
  43. self.ae(str(s.line(4)), 'ab123')
  44. self.ae((s.cursor.x, s.cursor.y), (2, 4))
  45. def test_draw_char(self):
  46. # Test in line-wrap, non-insert mode
  47. s = self.create_screen()
  48. s.draw('ココx')
  49. self.ae(str(s.line(0)), 'ココx')
  50. self.ae(tuple(map(s.line(0).width, range(5))), (2, 0, 2, 0, 1))
  51. self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
  52. s.draw('ニチハ')
  53. self.ae(str(s.line(0)), 'ココx')
  54. self.ae(str(s.line(1)), 'ニチ')
  55. self.ae(str(s.line(2)), 'ハ')
  56. self.ae(s.cursor.x, 2), self.ae(s.cursor.y, 2)
  57. s.draw('Ƶ̧\u0308')
  58. self.ae(str(s.line(2)), 'ハƵ̧\u0308')
  59. self.ae(s.cursor.x, 3), self.ae(s.cursor.y, 2)
  60. s.draw('xy'), s.draw('\u0306')
  61. self.ae(str(s.line(2)), 'ハƵ̧\u0308xy\u0306')
  62. self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 2)
  63. s.draw('c' * 15)
  64. self.ae(str(s.line(0)), 'ニチ')
  65. # Now test without line-wrap
  66. s.reset(), s.reset_dirty()
  67. s.reset_mode(DECAWM)
  68. s.draw('0\u030612345\u03066789\u0306')
  69. self.ae(str(s.line(0)), '0\u03061239\u0306')
  70. self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
  71. s.draw('ab\u0306')
  72. self.ae(str(s.line(0)), '0\u0306123b\u0306')
  73. self.ae(s.cursor.x, 5), self.ae(s.cursor.y, 0)
  74. # Now test in insert mode
  75. s.reset(), s.reset_dirty()
  76. s.set_mode(IRM)
  77. s.draw('1\u03062345' * 5)
  78. s.cursor_back(5)
  79. self.ae(s.cursor.x, 0), self.ae(s.cursor.y, 4)
  80. s.reset_dirty()
  81. s.draw('a\u0306b')
  82. self.ae(str(s.line(4)), 'a\u0306b1\u030623')
  83. self.ae((s.cursor.x, s.cursor.y), (2, 4))
  84. def test_rep(self):
  85. s = self.create_screen()
  86. s.draw('a')
  87. parse_bytes(s, b'\x1b[b')
  88. self.ae(str(s.line(0)), 'aa')
  89. parse_bytes(s, b'\x1b[3b')
  90. self.ae(str(s.line(0)), 'a'*5)
  91. s.draw(' ')
  92. parse_bytes(s, b'\x1b[3b')
  93. self.ae(str(s.line(1)), ' '*4)
  94. def test_emoji_skin_tone_modifiers(self):
  95. s = self.create_screen()
  96. q = chr(0x1f469) + chr(0x1f3fd)
  97. s.draw(q)
  98. self.ae(str(s.line(0)), q)
  99. self.ae(s.cursor.x, 2)
  100. def test_regional_indicators(self):
  101. s = self.create_screen()
  102. flag = '\U0001f1ee\U0001f1f3'
  103. s.draw(flag)
  104. self.ae(str(s.line(0)), flag)
  105. self.ae(s.cursor.x, 2)
  106. s = self.create_screen()
  107. s.draw('a'), s.draw(flag[0]), s.draw('b')
  108. self.ae(str(s.line(0)), 'a' + flag[0] + 'b')
  109. self.ae(s.cursor.x, 4)
  110. def test_zwj(self):
  111. s = self.create_screen(cols=20)
  112. q = '\U0001f468\u200d\U0001f469\u200d\U0001f467\u200d\U0001f466'
  113. s.draw(q)
  114. self.ae(q, str(s.line(0)))
  115. self.ae(s.cursor.x, 8)
  116. for x in '\u200b\u200c\u200d':
  117. s = self.create_screen()
  118. q = f'X{x}Y'
  119. s.draw(q)
  120. self.ae(q, str(s.line(0)))
  121. self.ae(s.cursor.x, 2)
  122. def test_char_manipulation(self):
  123. s = self.create_screen()
  124. def init():
  125. s.reset(), s.reset_dirty()
  126. s.draw('abcde')
  127. s.cursor.bold = True
  128. s.cursor_back(4)
  129. s.reset_dirty()
  130. self.ae(s.cursor.x, 1)
  131. init()
  132. s.insert_characters(2)
  133. self.ae(str(s.line(0)), 'a bc')
  134. self.assertTrue(s.line(0).cursor_from(1).bold)
  135. s.cursor_back(1)
  136. s.insert_characters(20)
  137. self.ae(str(s.line(0)), '')
  138. s.draw('xココ')
  139. s.cursor_back(5)
  140. s.reset_dirty()
  141. s.insert_characters(1)
  142. self.ae(str(s.line(0)), ' xコ')
  143. c = Cursor()
  144. c.italic = True
  145. s.line(0).apply_cursor(c, 0, 5)
  146. self.ae(s.line(0).width(2), 2)
  147. self.assertTrue(s.line(0).cursor_from(2).italic)
  148. self.assertFalse(s.line(0).cursor_from(2).bold)
  149. init()
  150. s.delete_characters(2)
  151. self.ae(str(s.line(0)), 'ade')
  152. self.assertTrue(s.line(0).cursor_from(4).bold)
  153. self.assertFalse(s.line(0).cursor_from(2).bold)
  154. s = self.create_screen()
  155. s.set_margins(1, 2)
  156. s.cursor.y = 3
  157. s.draw('abcde')
  158. s.cursor.x = 0
  159. s.delete_characters(2)
  160. self.ae('cde', str(s.line(s.cursor.y)))
  161. init()
  162. s.erase_characters(2)
  163. self.ae(str(s.line(0)), 'a de')
  164. self.assertTrue(s.line(0).cursor_from(1).bold)
  165. self.assertFalse(s.line(0).cursor_from(4).bold)
  166. s.erase_characters(20)
  167. self.ae(str(s.line(0)), 'a')
  168. init()
  169. s.erase_in_line()
  170. self.ae(str(s.line(0)), 'a')
  171. self.assertTrue(s.line(0).cursor_from(1).bold)
  172. self.assertFalse(s.line(0).cursor_from(0).bold)
  173. init()
  174. s.erase_in_line(1)
  175. self.ae(str(s.line(0)), ' cde')
  176. init()
  177. s.erase_in_line(2)
  178. self.ae(str(s.line(0)), '')
  179. init()
  180. s.erase_in_line(2, True)
  181. self.ae((False, False, False, False, False), tuple(map(lambda i: s.line(0).cursor_from(i).bold, range(5))))
  182. def test_erase_in_screen(self):
  183. s = self.create_screen()
  184. def init():
  185. s.reset()
  186. s.draw('12345' * 5)
  187. s.reset_dirty()
  188. s.cursor.x, s.cursor.y = 2, 1
  189. s.cursor.bold = True
  190. self.ae(continuations(s), (True, True, True, True, False))
  191. def all_lines(s):
  192. return tuple(str(s.line(i)) for i in range(s.lines))
  193. def continuations(s):
  194. return tuple(s.line(i).last_char_has_wrapped_flag() for i in range(s.lines))
  195. init()
  196. s.erase_in_display(0)
  197. self.ae(all_lines(s), ('12345', '12', '', '', ''))
  198. self.ae(continuations(s), (True, False, False, False, False))
  199. init()
  200. s.erase_in_display(1)
  201. self.ae(all_lines(s), ('', ' 45', '12345', '12345', '12345'))
  202. self.ae(continuations(s), (False, True, True, True, False))
  203. init()
  204. s.erase_in_display(2)
  205. self.ae(all_lines(s), ('', '', '', '', ''))
  206. self.assertTrue(s.line(0).cursor_from(1).bold)
  207. self.ae(continuations(s), (False, False, False, False, False))
  208. init()
  209. s.erase_in_display(2, True)
  210. self.ae(all_lines(s), ('', '', '', '', ''))
  211. self.ae(continuations(s), (False, False, False, False, False))
  212. self.assertFalse(s.line(0).cursor_from(1).bold)
  213. def test_cursor_movement(self):
  214. s = self.create_screen()
  215. s.draw('12345' * 5)
  216. s.reset_dirty()
  217. s.cursor_up(2)
  218. self.ae((s.cursor.x, s.cursor.y), (4, 2))
  219. s.cursor_up1()
  220. self.ae((s.cursor.x, s.cursor.y), (0, 1))
  221. s.cursor_forward(3)
  222. self.ae((s.cursor.x, s.cursor.y), (3, 1))
  223. s.cursor_back()
  224. self.ae((s.cursor.x, s.cursor.y), (2, 1))
  225. s.cursor_down()
  226. self.ae((s.cursor.x, s.cursor.y), (2, 2))
  227. s.cursor_down1(5)
  228. self.ae((s.cursor.x, s.cursor.y), (0, 4))
  229. s = self.create_screen()
  230. s.draw('12345' * 5)
  231. s.index()
  232. self.ae(str(s.line(4)), '')
  233. for i in range(4):
  234. self.ae(str(s.line(i)), '12345')
  235. s.draw('12345' * 5)
  236. s.cursor_up(5)
  237. s.reverse_index()
  238. self.ae(str(s.line(0)), '')
  239. for i in range(1, 5):
  240. self.ae(str(s.line(i)), '12345')
  241. def test_backspace_wide_characters(self):
  242. s = self.create_screen()
  243. s.draw('⛅')
  244. self.ae(s.cursor.x, 2)
  245. s.backspace()
  246. s.draw(' ')
  247. s.backspace()
  248. self.ae(s.cursor.x, 1)
  249. def test_resize(self):
  250. from kitty.window import as_text
  251. def at():
  252. return as_text(s, add_history=True)
  253. def ac():
  254. return s.line(s.cursor.y)[s.cursor.x]
  255. # test that a wrapped line split by the history buffer is re-stitched
  256. s = self.create_screen(cols=4, lines=4, scrollback=4)
  257. text = ''
  258. for i in range(s.lines + 1):
  259. if i == 2:
  260. text += 'abcd'
  261. else:
  262. text += str(i + 1) * s.columns
  263. s.draw(text)
  264. self.assertTrue(s.historybuf.endswith_wrap())
  265. s.cursor.x, s.cursor.y = 1, 1
  266. self.ae(ac(), 'b')
  267. s.resize(s.lines, s.columns + 2)
  268. self.assertTrue(s.historybuf.endswith_wrap())
  269. self.ae(str(s.historybuf), '111122')
  270. self.ae(at(), text + '\n')
  271. # for some reason rewrap_inner moves the cursor by one cell to the right
  272. self.ae((s.cursor.x, s.cursor.y), (4, 0))
  273. self.ae(ac(), 'c')
  274. s = self.create_screen(cols=4, lines=4, scrollback=4)
  275. s.draw('1111222'), s.linefeed(), s.carriage_return()
  276. s.draw('333344445555')
  277. s.resize(s.lines, s.columns + 2)
  278. self.ae(str(s.historybuf), '111122')
  279. self.ae(str(s.line(0)), '2')
  280. self.ae(at(), '1111222\n333344445555\n')
  281. s = self.create_screen(cols=4, lines=4, scrollback=4)
  282. s.draw('1111😸2'), s.linefeed(), s.carriage_return()
  283. s.index(), s.index()
  284. s.resize(s.lines, s.columns + 1)
  285. self.ae(str(s.historybuf), '1111')
  286. self.assertTrue(s.historybuf.endswith_wrap())
  287. self.ae(at(), '1111😸2\n\n\n')
  288. s = self.create_screen(cols=4, lines=4, scrollback=4)
  289. s.draw(text)
  290. s.cursor.x, s.cursor.y = 1, 1
  291. self.ae(ac(), 'b')
  292. s.resize(s.lines, s.columns * 2)
  293. self.ae(ac(), 'c')
  294. self.ae(str(s.historybuf), '11112222')
  295. self.ae(at(), text + '\n\n')
  296. self.ae((s.cursor.x, s.cursor.y), (2, 0))
  297. # test that trailing blank line is preserved on resize
  298. s = self.create_screen(cols=5, lines=5, scrollback=15)
  299. for i in range(3):
  300. s.draw(f'oo{i}'), s.index(), s.carriage_return()
  301. s.draw('$ pp'), s.index(), s.carriage_return()
  302. s.resize(s.lines, 2)
  303. self.assertFalse(str(s.line(s.cursor.y)))
  304. self.assertFalse(s.cursor.x)
  305. # test that only happens when last line is not continued
  306. s = self.create_screen(cols=5, lines=5, scrollback=15)
  307. for i in range(3):
  308. s.draw(f'oo{i}'), s.index(), s.carriage_return()
  309. s.draw('p' * (s.columns + 2)), s.carriage_return()
  310. s.resize(s.lines, 2)
  311. self.assertTrue(str(s.line(s.cursor.y)))
  312. s = self.create_screen(scrollback=6)
  313. s.draw(''.join([str(i) * s.columns for i in range(s.lines)]))
  314. s.resize(3, 10)
  315. self.ae(str(s.line(0)), '0'*5 + '1'*5)
  316. self.ae(str(s.line(1)), '2'*5 + '3'*5)
  317. self.ae(str(s.line(2)), '4'*5)
  318. s.resize(5, 1)
  319. self.ae(str(s.line(0)), '4')
  320. self.ae(str(s.historybuf), '3\n3\n3\n3\n3\n2')
  321. s = self.create_screen(scrollback=20)
  322. s.draw(''.join(str(i) * s.columns for i in range(s.lines*2)))
  323. self.ae(str(s.linebuf), '55555\n66666\n77777\n88888\n99999')
  324. s.resize(5, 2)
  325. self.ae(str(s.linebuf), '88\n88\n99\n99\n9')
  326. s = self.create_screen()
  327. s.draw('a' * s.columns)
  328. s.linefeed(), s.carriage_return()
  329. s.draw('bb')
  330. s.resize(s.lines, s.columns - 2)
  331. self.ae(str(s.linebuf), 'aaa\naa\nbb\n\n')
  332. s.cursor.y = s.cursor.x = 0
  333. s.draw('x' * len(str(s.line(0))))
  334. s.linefeed(), s.carriage_return()
  335. s.draw('x' * len(str(s.line(1))))
  336. s.resize(s.lines, s.columns + 4)
  337. self.ae(str(s.linebuf), 'xxx\nxx\nbb\n\n')
  338. s = self.create_screen()
  339. c = s.callbacks
  340. parse_bytes(s, b'\x1b[?2048$p') # ]
  341. self.ae(c.wtcbuf, b'\x1b[?2048;2$y') # ]
  342. c.clear()
  343. parse_bytes(s, b'\x1b[?2048h\x1b[?2048$p') # ]]
  344. self.ae(c.wtcbuf, b'\x1b[?2048;1$y') # ]
  345. self.ae(c.num_of_resize_events, 1)
  346. parse_bytes(s, b'\x1b[?2048h') # ]
  347. self.ae(c.num_of_resize_events, 2)
  348. def test_cursor_after_resize(self):
  349. def draw(text, end_line=True):
  350. s.draw(text)
  351. if end_line:
  352. s.linefeed(), s.carriage_return()
  353. s = self.create_screen()
  354. draw('123'), draw('123')
  355. y_before = s.cursor.y
  356. s.resize(s.lines, s.columns-1)
  357. self.ae(y_before, s.cursor.y)
  358. s = self.create_screen(cols=5, lines=8)
  359. draw('one')
  360. draw('two three four five |||', end_line=False)
  361. s.resize(s.lines + 2, s.columns + 2)
  362. y = s.cursor.y
  363. self.assertIn('|', str(s.line(y)))
  364. s = self.create_screen()
  365. draw('a')
  366. x_before = s.cursor.x
  367. s.resize(s.lines - 1, s.columns)
  368. self.ae(x_before, s.cursor.x)
  369. s = self.create_screen()
  370. s.draw('abc')
  371. b = s.cursor.x
  372. s.resize(7, s.columns)
  373. self.assertEqual(s.cursor.x, b)
  374. s.cursor.x = 0
  375. s.resize(5, s.columns)
  376. self.assertEqual(s.cursor.x, 0)
  377. def test_scrollback_fill_after_resize(self):
  378. def prepare_screen(content=()):
  379. ans = self.create_screen(options={'scrollback_fill_enlarged_window': True})
  380. for line in content:
  381. ans.draw(line)
  382. ans.linefeed()
  383. ans.carriage_return()
  384. return ans
  385. def assert_lines(*lines):
  386. return self.ae(lines, tuple(str(s.line(i)) for i in range(s.lines)))
  387. # test the reverse scroll function
  388. s = prepare_screen(map(str, range(6)))
  389. assert_lines('2', '3', '4', '5', '')
  390. s.reverse_scroll(2, True)
  391. assert_lines('0', '1', '2', '3', '4')
  392. # Height increased, width unchanged → pull down lines to fill new space at the top
  393. s = prepare_screen(map(str, range(6)))
  394. assert_lines('2', '3', '4', '5', '')
  395. dist_from_bottom = s.lines - s.cursor.y
  396. s.resize(7, s.columns)
  397. assert_lines('0', '1', '2', '3', '4', '5', '')
  398. self.ae(dist_from_bottom, s.lines - s.cursor.y)
  399. # Height increased, width increased → rewrap, pull down
  400. s = prepare_screen(['0', '1', '2', '3' * 15])
  401. assert_lines('2', '33333', '33333', '33333', '')
  402. s.resize(7, 12)
  403. assert_lines('0', '1', '2', '333333333333', '333', '', '')
  404. # Height increased, width decreased → rewrap, pull down if possible
  405. s = prepare_screen(['0', '1', '2', '3' * 5])
  406. assert_lines('0', '1', '2', '33333', '')
  407. s.resize(6, 4)
  408. assert_lines('0', '1', '2', '3333', '3', '')
  409. # Height unchanged, width increased → rewrap, pull down if possible
  410. s = prepare_screen(['0', '1', '2', '3' * 15])
  411. assert_lines('2', '33333', '33333', '33333', '')
  412. s.resize(s.lines, 12)
  413. assert_lines('1', '2', '333333333333', '333', '')
  414. # Height decreased, width increased → rewrap, pull down if possible
  415. s = prepare_screen(['0', '1', '2', '3' * 15])
  416. assert_lines('2', '33333', '33333', '33333', '')
  417. s.resize(4, 12)
  418. assert_lines('2', '333333333333', '333', '')
  419. # Height increased with large continued text
  420. s = self.create_screen(options={'scrollback_fill_enlarged_window': True})
  421. s.draw(('x' * (s.columns * s.lines * 2)) + 'abcde')
  422. s.carriage_return(), s.linefeed()
  423. s.draw('>')
  424. assert_lines('xxxxx', 'xxxxx', 'xxxxx', 'abcde', '>')
  425. s.resize(s.lines + 2, s.columns)
  426. assert_lines('xxxxx', 'xxxxx', 'xxxxx', 'xxxxx', 'xxxxx', 'abcde', '>')
  427. def test_tab_stops(self):
  428. # Taken from vttest/main.c
  429. s = self.create_screen(cols=80, lines=2)
  430. s.cursor_position(1, 1)
  431. s.clear_tab_stop(3)
  432. for col in range(1, s.columns - 1, 3):
  433. s.cursor_forward(3)
  434. s.set_tab_stop()
  435. s.cursor_position(1, 4)
  436. for col in range(4, s.columns - 1, 6):
  437. s.clear_tab_stop(0)
  438. s.cursor_forward(6)
  439. s.cursor_position(1, 7)
  440. s.clear_tab_stop(2)
  441. s.cursor_position(1, 1)
  442. for col in range(1, s.columns - 1, 6):
  443. s.tab()
  444. s.draw('*')
  445. s.cursor_position(2, 2)
  446. self.ae(str(s.line(0)), '\t*'*13)
  447. s = self.create_screen(cols=4, lines=2)
  448. s.draw('aaaX\tbbbb')
  449. self.ae(str(s.line(0)) + str(s.line(1)), 'aaaXbbbb')
  450. def test_margins(self):
  451. # Taken from vttest/main.c
  452. s = self.create_screen(cols=80, lines=24)
  453. def nl():
  454. s.carriage_return(), s.linefeed()
  455. for deccolm in (False, True):
  456. if deccolm:
  457. s.resize(24, 132)
  458. s.set_mode(DECCOLM)
  459. else:
  460. s.reset_mode(DECCOLM)
  461. region = s.lines - 6
  462. s.set_margins(3, region + 3)
  463. s.set_mode(DECOM)
  464. for i in range(26):
  465. ch = chr(ord('A') + i)
  466. which = i % 4
  467. if which == 0:
  468. s.cursor_position(region + 1, 1), s.draw(ch)
  469. s.cursor_position(region + 1, s.columns), s.draw(ch.lower())
  470. nl()
  471. elif which == 1:
  472. # Simple wrapping
  473. s.cursor_position(region, s.columns), s.draw(chr(ord('A') + i - 1).lower() + ch)
  474. # Backspace at right margin
  475. s.cursor_position(region + 1, s.columns), s.draw(ch), s.backspace(), s.draw(ch.lower())
  476. nl()
  477. elif which == 2:
  478. # Tab to right margin
  479. s.cursor_position(region + 1, s.columns), s.draw(ch), s.backspace(), s.backspace(), s.tab(), s.tab(), s.draw(ch.lower())
  480. s.cursor_position(region + 1, 2), s.backspace(), s.draw(ch), nl()
  481. else:
  482. s.cursor_position(region + 1, 1), nl()
  483. s.cursor_position(region, 1), s.draw(ch)
  484. s.cursor_position(region, s.columns), s.draw(ch.lower())
  485. for ln in range(2, region + 2):
  486. c = chr(ord('I') + ln - 2)
  487. before = '\t' if ln % 4 == 0 else ' '
  488. self.ae(c + ' ' * (s.columns - 3) + before + c.lower(), str(s.line(ln)))
  489. s.reset_mode(DECOM)
  490. # Test that moving cursor outside the margins works as expected
  491. s = self.create_screen(10, 10)
  492. s.set_margins(4, 6)
  493. s.cursor_position(0, 0)
  494. self.ae(s.cursor.y, 0)
  495. nl()
  496. self.ae(s.cursor.y, 1)
  497. s.cursor.y = s.lines - 1
  498. self.ae(s.cursor.y, 9)
  499. s.reverse_index()
  500. self.ae(s.cursor.y, 8)
  501. def test_sgr(self):
  502. s = self.create_screen()
  503. s.select_graphic_rendition(0, 1, 37, 42)
  504. s.draw('a')
  505. c = s.line(0).cursor_from(0)
  506. self.assertTrue(c.bold)
  507. self.ae(c.bg, (2 << 8) | 1)
  508. s.cursor_position(2, 1)
  509. s.select_graphic_rendition(0, 35)
  510. s.draw('b')
  511. c = s.line(1).cursor_from(0)
  512. self.ae(c.fg, (5 << 8) | 1)
  513. self.ae(c.bg, 0)
  514. s.cursor_position(2, 2)
  515. s.select_graphic_rendition(38, 2, 99, 1, 2, 3)
  516. s.draw('c')
  517. c = s.line(1).cursor_from(1)
  518. self.ae(c.fg, (1 << 24) | (2 << 16) | (3 << 8) | 2)
  519. def test_cursor_hidden(self):
  520. s = self.create_screen()
  521. s.toggle_alt_screen()
  522. s.cursor_visible = False
  523. s.toggle_alt_screen()
  524. self.assertFalse(s.cursor_visible)
  525. def test_dirty_lines(self):
  526. s = self.create_screen()
  527. self.assertFalse(s.linebuf.dirty_lines())
  528. s.draw('a' * (s.columns * 2))
  529. self.ae(s.linebuf.dirty_lines(), [0, 1])
  530. self.assertFalse(s.historybuf.dirty_lines())
  531. while not s.historybuf.count:
  532. s.draw('a' * (s.columns * 2))
  533. self.ae(s.historybuf.dirty_lines(), list(range(s.historybuf.count)))
  534. self.ae(s.linebuf.dirty_lines(), list(range(s.lines)))
  535. s.cursor.x, s.cursor.y = 0, 1
  536. s.insert_lines(2)
  537. self.ae(s.linebuf.dirty_lines(), [0, 3, 4])
  538. s.draw('a' * (s.columns * s.lines))
  539. self.ae(s.linebuf.dirty_lines(), list(range(s.lines)))
  540. s.cursor.x, s.cursor.y = 0, 1
  541. s.delete_lines(2)
  542. self.ae(s.linebuf.dirty_lines(), [0, 1, 2])
  543. s = self.create_screen()
  544. self.assertFalse(s.linebuf.dirty_lines())
  545. s.erase_in_line(0, False)
  546. self.ae(s.linebuf.dirty_lines(), [0])
  547. s.index(), s.index()
  548. s.erase_in_display(0, False)
  549. self.ae(s.linebuf.dirty_lines(), [0, 2, 3, 4])
  550. s = self.create_screen()
  551. self.assertFalse(s.linebuf.dirty_lines())
  552. s.insert_characters(2)
  553. self.ae(s.linebuf.dirty_lines(), [0])
  554. s.cursor.y = 1
  555. s.delete_characters(2)
  556. self.ae(s.linebuf.dirty_lines(), [0, 1])
  557. s.cursor.y = 2
  558. s.erase_characters(2)
  559. self.ae(s.linebuf.dirty_lines(), [0, 1, 2])
  560. def test_selection_as_text(self):
  561. s = self.create_screen()
  562. for i in range(2 * s.lines):
  563. if i != 0:
  564. s.carriage_return(), s.linefeed()
  565. s.draw(str(i) * s.columns)
  566. s.start_selection(0, 0)
  567. s.update_selection(4, 4)
  568. def ts(*args):
  569. return ''.join(s.text_for_selection(*args))
  570. expected = ''.join(('55555', '\n66666', '\n77777', '\n88888', '\n99999'))
  571. self.ae(ts(), expected)
  572. s.scroll(2, True)
  573. self.ae(ts(), expected)
  574. s.reset()
  575. s.draw('ab cd')
  576. s.start_selection(0, 0)
  577. s.update_selection(1, 3)
  578. self.ae(ts(), ''.join(('ab ', 'cd')))
  579. self.ae(ts(False, True), ''.join(('ab', 'cd')))
  580. s.reset()
  581. s.draw('ab cd')
  582. s.start_selection(0, 0)
  583. s.update_selection(3, 4)
  584. self.ae(s.text_for_selection(), ('ab ', ' ', 'cd'))
  585. self.ae(s.text_for_selection(False, True), ('ab', '\n', 'cd'))
  586. s.reset()
  587. s.draw('a')
  588. s.select_graphic_rendition(32)
  589. s.draw('b')
  590. s.select_graphic_rendition(39)
  591. s.draw('c xy')
  592. s.start_selection(0, 0)
  593. s.update_selection(1, 3)
  594. self.ae(s.text_for_selection(), ('abc ', 'xy'))
  595. self.ae(s.text_for_selection(True), ('a\x1b[32mb\x1b[39mc ', 'xy', '\x1b[m'))
  596. self.ae(s.text_for_selection(True, True), ('a\x1b[32mb\x1b[39mc', 'xy', '\x1b[m'))
  597. # ]]]]]]]]]]]]]]]]]]]]
  598. def test_soft_hyphen(self):
  599. s = self.create_screen()
  600. s.draw('a\u00adb')
  601. self.ae(s.cursor.x, 2)
  602. s.start_selection(0, 0)
  603. s.update_selection(2, 0)
  604. self.ae(s.text_for_selection(), ('a\u00adb',))
  605. def test_variation_selectors(self):
  606. s = self.create_screen()
  607. def t(*a):
  608. s.reset()
  609. for i in range(0, len(a), 2):
  610. char, x = a[i], a[i+1]
  611. s.draw(char)
  612. self.ae(s.cursor.x, x, f'after char: {char!r}')
  613. # already wide + VS15
  614. t('\U0001f610', 2, '\ufe0e', 1, '\ufe0e', 1)
  615. t('\U0001f610\ufe0e', 1, '\ufe0e', 1)
  616. # narrow + VS16
  617. t('\u25b6', 1, '\ufe0f', 2)
  618. t('\u25b6\ufe0f', 2)
  619. # wide + VS16
  620. t('\u26d4\ufe0f', 2, '\ufe0f', 2)
  621. t('\u26d4', 2, '\ufe0f', 2)
  622. # narrow + VS15
  623. t('\u25b6', 1, '\ufe0e', 1)
  624. t('\u25b6\ufe0e', 1)
  625. # narrow + VS16 + VS15
  626. t('\u25b6', 1, '\ufe0f', 2, '\ufe0e', 2)
  627. # wide + VS15 + VS16
  628. t('\U0001f610', 2, '\ufe0e', 1, '\ufe0f', 1)
  629. def test_writing_with_cursor_on_trailer_of_wide_character(self):
  630. s = self.create_screen()
  631. def r(x, pos, expected):
  632. s.reset()
  633. s.draw('😸')
  634. self.ae(s.cursor.x, 2)
  635. s.cursor.x = 1
  636. s.draw(x)
  637. self.ae(s.cursor.x, pos)
  638. self.ae(str(s.line(0)), expected)
  639. r('a', 2, ' a')
  640. r('😸', 3, ' 😸')
  641. r('\u0304', 1, '😸\u0304')
  642. r('\r', 0, '😸')
  643. def test_serialize(self):
  644. from kitty.window import as_text
  645. s = self.create_screen()
  646. parse_bytes(s, b'\x1b[1;91m')
  647. s.draw('X')
  648. parse_bytes(s, b'\x1b[0m\x1b[2m')
  649. s.draw('Y')
  650. self.ae(as_text(s, True), '\x1b[m\x1b[22;1;91mX\x1b[22;2;39mY\n\n\n\n')
  651. s.reset()
  652. s.draw('ab' * s.columns)
  653. s.carriage_return(), s.linefeed()
  654. s.draw('c')
  655. self.ae(as_text(s), 'ababababab\nc\n\n')
  656. self.ae(as_text(s, True), '\x1b[mababa\x1b[mbabab\n\x1b[mc\n\n')
  657. s = self.create_screen(cols=2, lines=2, scrollback=2)
  658. for i in range(1, 7):
  659. s.select_graphic_rendition(30 + i)
  660. s.draw(f'{i}' * s.columns)
  661. self.ae(as_text(s, True, True), '\x1b[m\x1b[31m11\x1b[m\x1b[32m22\x1b[m\x1b[33m33\x1b[m\x1b[34m44\x1b[m\x1b[m\x1b[35m55\x1b[m\x1b[36m66')
  662. def set_link(url=None, id=None):
  663. parse_bytes(s, '\x1b]8;id={};{}\x1b\\'.format(id or '', url or '').encode('utf-8'))
  664. s = self.create_screen()
  665. s.draw('a')
  666. set_link('moo', 'foo')
  667. s.draw('bcdef')
  668. self.ae(as_text(s, True), '\x1b[ma\x1b]8;id=foo;moo\x1b\\bcde\x1b[mf\n\n\n\x1b]8;;\x1b\\')
  669. set_link()
  670. s.draw('gh')
  671. self.ae(as_text(s, True), '\x1b[ma\x1b]8;id=foo;moo\x1b\\bcde\x1b[mf\x1b]8;;\x1b\\gh\n\n\n')
  672. s = self.create_screen()
  673. s.draw('a')
  674. set_link('moo')
  675. s.draw('bcdef')
  676. self.ae(as_text(s, True), '\x1b[ma\x1b]8;;moo\x1b\\bcde\x1b[mf\n\n\n\x1b]8;;\x1b\\')
  677. def test_wrapping_serialization(self):
  678. from kitty.window import as_text
  679. s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128})
  680. s.draw('ū̀abbccddeefū̀')
  681. self.ae(as_text(s, add_history=True), 'ū̀abbccddeefū̀')
  682. self.assertNotIn('\n', as_text(s, add_history=True, as_ansi=True))
  683. s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128})
  684. s.draw('1'), s.carriage_return(), s.linefeed()
  685. s.draw('2'), s.carriage_return(), s.linefeed()
  686. s.draw('3'), s.carriage_return(), s.linefeed()
  687. s.draw('4'), s.carriage_return(), s.linefeed()
  688. s.draw('5'), s.carriage_return(), s.linefeed()
  689. s.draw('6'), s.carriage_return(), s.linefeed()
  690. s.draw('7')
  691. self.ae(as_text(s, add_history=True), '1\n2\n3\n4\n5\n6\n7')
  692. s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128})
  693. s.draw('aabb')
  694. s.cursor.y = 0
  695. s.carriage_return(), s.linefeed()
  696. self.ae(as_text(s, add_history=True), 'aabb')
  697. s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128})
  698. s.draw('a'), s.carriage_return(), s.linefeed()
  699. s.cursor.y = 0
  700. s.draw('aabb')
  701. self.ae(as_text(s), 'aabb')
  702. s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128})
  703. s.draw('a😀')
  704. self.ae(as_text(s), 'a😀')
  705. def test_pagerhist(self):
  706. hsz = 8
  707. s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': hsz})
  708. def contents():
  709. return s.historybuf.pagerhist_as_text()
  710. def line(i):
  711. q.append('\x1b[m' + f'{i}' * s.columns + '\r')
  712. def w(x):
  713. s.historybuf.pagerhist_write(x)
  714. def test():
  715. expected = ''.join(q)
  716. maxlen = hsz
  717. extra = len(expected) - maxlen
  718. if extra > 0:
  719. expected = expected[extra:]
  720. got = contents()
  721. self.ae(got, expected)
  722. q = []
  723. for i in range(4):
  724. s.draw(f'{i}' * s.columns)
  725. self.ae(contents(), '')
  726. s.draw('4' * s.columns), line(0), test()
  727. s.draw('5' * s.columns), line(1), test()
  728. s.draw('6' * s.columns), line(2), test()
  729. s.draw('7' * s.columns), line(3), test()
  730. s.draw('8' * s.columns), line(4), test()
  731. s.draw('9' * s.columns), line(5), test()
  732. s = self.create_screen(options={'scrollback_pager_history_size': 2048})
  733. text = '\x1b[msoft\r\x1b[mbreak\nnext😼cat'
  734. w(text)
  735. self.ae(contents(), text)
  736. s.historybuf.pagerhist_rewrap(2)
  737. self.ae(contents(), '\x1b[mso\rft\x1b[m\rbr\rea\rk\nne\rxt\r😼\rca\rt')
  738. s = self.create_screen(options={'scrollback_pager_history_size': 8})
  739. w('😼')
  740. self.ae(contents(), '😼')
  741. w('abcd')
  742. self.ae(contents(), '😼abcd')
  743. w('e')
  744. self.ae(contents(), 'abcde')
  745. def test_user_marking(self):
  746. def cells(*a, y=0, mark=3):
  747. return [(x, y, mark) for x in a]
  748. s = self.create_screen()
  749. s.draw('abaa')
  750. s.carriage_return(), s.linefeed()
  751. s.draw('xyxyx')
  752. s.set_marker(marker_from_regex('a', 3))
  753. self.ae(s.marked_cells(), cells(0, 2, 3))
  754. s.set_marker()
  755. self.ae(s.marked_cells(), [])
  756. def mark_x(text):
  757. col = 0
  758. for i, c in enumerate(text):
  759. if c == 'x':
  760. col += 1
  761. yield i, i, col
  762. s.set_marker(marker_from_function(mark_x))
  763. self.ae(s.marked_cells(), [(0, 1, 1), (2, 1, 2), (4, 1, 3)])
  764. s = self.create_screen(lines=5, scrollback=10)
  765. for i in range(15):
  766. s.draw(str(i))
  767. if i != 14:
  768. s.carriage_return(), s.linefeed()
  769. s.set_marker(marker_from_regex(r'\d+', 3))
  770. for i in range(10):
  771. self.assertTrue(s.scroll_to_next_mark())
  772. self.ae(s.scrolled_by, i + 1)
  773. self.ae(s.scrolled_by, 10)
  774. for i in range(10):
  775. self.assertTrue(s.scroll_to_next_mark(0, False))
  776. self.ae(s.scrolled_by, 10 - i - 1)
  777. self.ae(s.scrolled_by, 0)
  778. s = self.create_screen()
  779. s.draw('🐈ab')
  780. s.set_marker(marker_from_regex('🐈', 3))
  781. self.ae(s.marked_cells(), cells(0, 1))
  782. s.set_marker(marker_from_regex('🐈a', 3))
  783. self.ae(s.marked_cells(), cells(0, 1, 2))
  784. s.set_marker(marker_from_regex('a', 3))
  785. self.ae(s.marked_cells(), cells(2))
  786. s = self.create_screen(cols=20)
  787. s.tab()
  788. s.draw('ab')
  789. s.set_marker(marker_from_regex('a', 3))
  790. self.ae(s.marked_cells(), cells(8))
  791. s.set_marker(marker_from_regex('\t', 3))
  792. self.ae(s.marked_cells(), cells(*range(8)))
  793. s = self.create_screen()
  794. s.cursor.x = 2
  795. s.draw('x')
  796. s.cursor.x += 1
  797. s.draw('x')
  798. s.set_marker(marker_from_function(mark_x))
  799. self.ae(s.marked_cells(), [(2, 0, 1), (4, 0, 2)])
  800. def test_hyperlinks(self):
  801. s = self.create_screen()
  802. self.ae(s.line(0).hyperlink_ids(), tuple(0 for x in range(s.columns)))
  803. def set_link(url=None, id=None):
  804. parse_bytes(s, '\x1b]8;id={};{}\x1b\\'.format(id or '', url or '').encode('utf-8'))
  805. set_link('url-a', 'a')
  806. self.ae(s.line(0).hyperlink_ids(), tuple(0 for x in range(s.columns)))
  807. s.draw('a')
  808. self.ae(s.line(0).hyperlink_ids(), (1,) + tuple(0 for x in range(s.columns - 1)))
  809. s.draw('bc')
  810. self.ae(s.line(0).hyperlink_ids(), (1, 1, 1, 0, 0))
  811. set_link()
  812. s.draw('d')
  813. self.ae(s.line(0).hyperlink_ids(), (1, 1, 1, 0, 0))
  814. set_link('url-a', 'a')
  815. s.draw('efg')
  816. self.ae(s.line(0).hyperlink_ids(), (1, 1, 1, 0, 1))
  817. self.ae(s.line(1).hyperlink_ids(), (1, 1, 0, 0, 0))
  818. set_link('url-b')
  819. s.draw('hij')
  820. self.ae(s.line(1).hyperlink_ids(), (1, 1, 2, 2, 2))
  821. set_link()
  822. self.ae({('a:url-a', 1), (':url-b', 2)}, s.hyperlinks_as_set())
  823. s.garbage_collect_hyperlink_pool()
  824. self.ae({('a:url-a', 1), (':url-b', 2)}, s.hyperlinks_as_set())
  825. for i in range(s.lines + 2):
  826. s.linefeed()
  827. s.garbage_collect_hyperlink_pool()
  828. self.ae({('a:url-a', 1), (':url-b', 2)}, s.hyperlinks_as_set())
  829. for i in range(s.lines * 2):
  830. s.linefeed()
  831. s.garbage_collect_hyperlink_pool()
  832. self.assertFalse(s.hyperlinks_as_set())
  833. set_link('url-a', 'x')
  834. s.draw('a')
  835. set_link('url-a', 'y')
  836. s.draw('a')
  837. set_link()
  838. self.ae({('x:url-a', 1), ('y:url-a', 2)}, s.hyperlinks_as_set())
  839. s = self.create_screen()
  840. set_link('u' * 2048)
  841. s.draw('a')
  842. self.ae({(':' + 'u' * 2045, 1)}, s.hyperlinks_as_set())
  843. s = self.create_screen()
  844. set_link('u' * 2048, 'i' * 300)
  845. s.draw('a')
  846. self.ae({('i'*256 + ':' + 'u' * (2045 - 256), 1)}, s.hyperlinks_as_set())
  847. s = self.create_screen()
  848. set_link('1'), s.draw('1')
  849. set_link('2'), s.draw('2')
  850. set_link('3'), s.draw('3')
  851. s.cursor.x = 1
  852. set_link(), s.draw('X')
  853. self.ae(s.line(0).hyperlink_ids(), (1, 0, 3, 0, 0))
  854. self.ae({(':1', 1), (':2', 2), (':3', 3)}, s.hyperlinks_as_set())
  855. s.garbage_collect_hyperlink_pool()
  856. self.ae({(':1', 1), (':3', 2)}, s.hyperlinks_as_set())
  857. set_link('3'), s.draw('3')
  858. self.ae({(':1', 1), (':3', 2)}, s.hyperlinks_as_set())
  859. set_link('4'), s.draw('4')
  860. self.ae({(':1', 1), (':3', 2), (':4', 3)}, s.hyperlinks_as_set())
  861. s = self.create_screen()
  862. set_link('1'), s.draw('1')
  863. set_link('2'), s.draw('2')
  864. set_link('1'), s.draw('1')
  865. self.ae({(':2', 2), (':1', 1)}, s.hyperlinks_as_set())
  866. s = self.create_screen()
  867. set_link('1'), s.draw('12'), set_link(), s.draw('X'), set_link('1'), s.draw('3')
  868. s.linefeed(), s.carriage_return()
  869. s.draw('abc')
  870. s.linefeed(), s.carriage_return()
  871. set_link(), s.draw('Z ')
  872. set_link('1'), s.draw('xyz')
  873. s.linefeed(), s.carriage_return()
  874. set_link('2'), s.draw('Z Z')
  875. self.assertIsNone(s.current_url_text())
  876. self.assertIsNone(s.hyperlink_at(0, 4))
  877. self.assertIsNone(s.current_url_text())
  878. self.ae(s.hyperlink_at(0, 0), '1')
  879. self.ae(s.current_url_text(), '123abcxyz')
  880. self.ae('1', s.hyperlink_at(3, 2))
  881. self.ae(s.current_url_text(), '123abcxyz')
  882. self.ae('2', s.hyperlink_at(1, 3))
  883. self.ae(s.current_url_text(), 'Z Z')
  884. def test_bottom_margin(self):
  885. s = self.create_screen(cols=80, lines=6, scrollback=4)
  886. s.set_margins(0, 5)
  887. for i in range(8):
  888. s.draw(str(i))
  889. s.linefeed()
  890. s.carriage_return()
  891. self.ae(str(s.linebuf), '4\n5\n6\n7\n\n')
  892. self.ae(str(s.historybuf), '3\n2\n1\n0')
  893. def test_top_margin(self):
  894. s = self.create_screen(cols=80, lines=6, scrollback=4)
  895. s.set_margins(2, 6)
  896. for i in range(8):
  897. s.draw(str(i))
  898. s.linefeed()
  899. s.carriage_return()
  900. self.ae(str(s.linebuf), '0\n4\n5\n6\n7\n')
  901. self.ae(str(s.historybuf), '')
  902. def test_top_and_bottom_margin(self):
  903. s = self.create_screen(cols=80, lines=6, scrollback=4)
  904. s.set_margins(2, 5)
  905. for i in range(8):
  906. s.draw(str(i))
  907. s.linefeed()
  908. s.carriage_return()
  909. self.ae(str(s.linebuf), '0\n5\n6\n7\n\n')
  910. self.ae(str(s.historybuf), '')
  911. def test_osc_52(self):
  912. s = self.create_screen()
  913. c = s.callbacks
  914. def send(what: str):
  915. return parse_bytes(s, f'\033]52;p;{what}\a'.encode('ascii'))
  916. def t(q, *expected):
  917. c.clear()
  918. send(q)
  919. del q
  920. t.ex = list(expected)
  921. del expected
  922. try:
  923. self.ae(tuple(map(len, c.cc_buf)), tuple(map(len, t.ex)))
  924. self.ae(c.cc_buf, t.ex)
  925. finally:
  926. del t.ex
  927. t('XYZ', ('p;XYZ', False))
  928. t('a' * VT_PARSER_BUFFER_SIZE, ('p;' + 'a' * (VT_PARSER_BUFFER_SIZE - 8), True), (';' + 'a' * 8, False))
  929. t('', ('p;', False))
  930. t('!', ('p;!', False))
  931. def test_key_encoding_flags_stack(self):
  932. s = self.create_screen()
  933. c = s.callbacks
  934. def w(code, p1='', p2=''):
  935. p = f'{p1}'
  936. if p2:
  937. p += f';{p2}'
  938. return parse_bytes(s, f'\033[{code}{p}u'.encode('ascii'))
  939. def ac(flags):
  940. parse_bytes(s, b'\033[?u')
  941. self.ae(c.wtcbuf, f'\033[?{flags}u'.encode('ascii'))
  942. c.clear()
  943. ac(0)
  944. w('=', 0b1001)
  945. ac(0b1001)
  946. w('=', 0b0011, 2)
  947. ac(0b1011)
  948. w('=', 0b0110, 3)
  949. ac(0b1001)
  950. s.reset()
  951. ac(0)
  952. w('>', 0b0011)
  953. ac(0b0011)
  954. w('=', 0b1111)
  955. ac(0b1111)
  956. w('>', 0b10)
  957. ac(0b10)
  958. w('<')
  959. ac(0b1111)
  960. for i in range(10):
  961. w('<')
  962. ac(0)
  963. s.reset()
  964. for i in range(1, 16):
  965. w('>', i)
  966. ac(15)
  967. w('<'), ac(14), w('<'), ac(13)
  968. def test_color_stack(self):
  969. s = self.create_screen()
  970. c = s.callbacks
  971. def w(code):
  972. return parse_bytes(s, ('\033[' + code).encode('ascii'))
  973. def ac(idx, count):
  974. self.ae(c.wtcbuf, f'\033[{idx};{count}#Q'.encode('ascii'))
  975. c.clear()
  976. # ]]]]]]]]]]]]]]]]}}}}}}}}}}}}}}}}))))))))))))))))))))))
  977. w('#R')
  978. ac(0, 0)
  979. w('#P')
  980. w('#R')
  981. ac(0, 1)
  982. w('10#P')
  983. w('#R')
  984. ac(0, 1)
  985. w('#Q')
  986. w('#R')
  987. ac(0, 0)
  988. for i in range(20):
  989. w('#P')
  990. w('#R')
  991. ac(9, 10)
  992. def test_detect_url(self):
  993. s = self.create_screen(cols=30)
  994. def ae(expected, x=3, y=0):
  995. s.detect_url(x, y)
  996. url = ''.join(s.text_for_marked_url())
  997. self.assertEqual(expected, url)
  998. def t(url, x=0, y=0, before='', after='', expected=''):
  999. s.reset()
  1000. s.cursor.x = x
  1001. s.cursor.y = y
  1002. s.draw(before + url + after)
  1003. ae(expected or url, x=x + 1 + len(before), y=y)
  1004. t('http://moo.com')
  1005. t('http://moo.com/something?else=+&what-')
  1006. t('http://moo.com#fragme')
  1007. for (st, e) in '() {} [] <>'.split():
  1008. t('http://moo.com', before=st, after=e)
  1009. for trailer in ')-=':
  1010. t('http://moo.com' + trailer)
  1011. for trailer in '{}([<>': # )]>
  1012. t('http://moo.com', after=trailer)
  1013. t('http://moo.com', x=s.columns - 9)
  1014. t('https://wraps-by-one-char.com', before='[', after=']')
  1015. t('http://[::1]:8080')
  1016. t('https://wr[aps-by-one-ch]ar.com')
  1017. t('http://[::1]:8080/x', after='[') # ]
  1018. t('http://[::1]:8080/x]y34', expected='http://[::1]:8080/x')
  1019. t('https://wraps-by-one-char.com[]/x', after='[') # ]
  1020. def test_prompt_marking(self):
  1021. # ]]]]]]]]]]]]]]]]}}}}}}}}}}}}}}}}))))))))))))))))))))))
  1022. def mark_prompt():
  1023. parse_bytes(s, b'\033]133;A\007')
  1024. def mark_output():
  1025. parse_bytes(s, b'\033]133;C\007')
  1026. def draw_prompt(x):
  1027. mark_prompt(), s.draw(f'$ {x}'), s.carriage_return(), s.index()
  1028. def draw_output(n, x='', m=True):
  1029. if m:
  1030. mark_output()
  1031. for i in range(n):
  1032. s.draw(f'{i}{x}'), s.index(), s.carriage_return()
  1033. s = self.create_screen(cols=5, lines=5, scrollback=15)
  1034. draw_output(3, 'oo')
  1035. draw_prompt('pp')
  1036. mark_output()
  1037. s.toggle_alt_screen()
  1038. s.resize(s.lines, 2)
  1039. s.toggle_alt_screen()
  1040. self.assertFalse(str(s.line(s.cursor.y)))
  1041. s = self.create_screen()
  1042. for i in range(4):
  1043. mark_prompt()
  1044. s.draw(f'$ {i}')
  1045. s.carriage_return()
  1046. s.index(), s.index()
  1047. self.ae(s.scrolled_by, 0)
  1048. self.assertTrue(s.scroll_to_prompt())
  1049. self.ae(str(s.visual_line(0)), '$ 1')
  1050. self.assertTrue(s.scroll_to_prompt())
  1051. self.ae(str(s.visual_line(0)), '$ 0')
  1052. self.assertFalse(s.scroll_to_prompt())
  1053. self.assertTrue(s.scroll_to_prompt(1))
  1054. self.ae(str(s.visual_line(0)), '$ 1')
  1055. self.assertTrue(s.scroll_to_prompt(1))
  1056. self.ae(str(s.visual_line(0)), '$ 2')
  1057. self.assertFalse(s.scroll_to_prompt(1))
  1058. s = self.create_screen()
  1059. mark_prompt(), s.draw('$ 0')
  1060. s.carriage_return(), s.index()
  1061. mark_prompt(), s.draw('$ 1')
  1062. for i in range(s.lines):
  1063. s.carriage_return(), s.index()
  1064. s.draw(str(i))
  1065. self.assertTrue(s.scroll_to_prompt())
  1066. self.ae(str(s.visual_line(0)), '$ 1')
  1067. def lco(as_ansi=False, which=0):
  1068. a = []
  1069. if s.cmd_output(which, a.append, as_ansi):
  1070. pht = pagerhist(s, as_ansi=as_ansi, upto_output_start=True)
  1071. if pht:
  1072. a.insert(0, pht)
  1073. return ''.join(a)
  1074. def fco():
  1075. a = []
  1076. s.cmd_output(1, a.append)
  1077. return ''.join(a)
  1078. def lvco():
  1079. a = []
  1080. s.cmd_output(2, a.append)
  1081. return ''.join(a)
  1082. s = self.create_screen()
  1083. s.draw('abcd'), s.index(), s.carriage_return()
  1084. s.draw('12'), s.index(), s.carriage_return()
  1085. self.ae(fco(), '')
  1086. self.ae(lco(), 'abcd\n12\n')
  1087. s = self.create_screen()
  1088. mark_prompt(), s.draw('$ 0')
  1089. s.carriage_return(), s.index()
  1090. mark_output()
  1091. s.draw('abcd'), s.index(), s.carriage_return()
  1092. s.draw('12'), s.index(), s.carriage_return()
  1093. mark_prompt(), s.draw('$ 1')
  1094. self.ae(fco(), 'abcd\n12')
  1095. self.ae(lco(), 'abcd\n12')
  1096. self.ae(lco(as_ansi=True), '\x1b[m\x1b]133;C\x1b\\abcd\n\x1b[m12') # ]]]
  1097. s = self.create_screen(cols=5, lines=5, scrollback=15)
  1098. draw_output(1, 'start', False)
  1099. draw_prompt('0'), draw_output(3)
  1100. draw_prompt('1')
  1101. draw_prompt('2'), draw_output(2, 'x')
  1102. # last cmd output
  1103. # test: find upwards
  1104. self.ae(s.scrolled_by, 0)
  1105. self.ae(lco(), '0x\n1x\n')
  1106. # get output after scroll up
  1107. s.scroll_to_prompt()
  1108. self.ae(s.scrolled_by, 4)
  1109. self.ae(str(s.visual_line(0)), '$ 0')
  1110. self.ae(lco(), '0x\n1x\n')
  1111. # first cmd output on screen
  1112. # test: find around
  1113. self.ae(fco(), '0\n1\n2')
  1114. s.scroll(2, False)
  1115. self.ae(s.scrolled_by, 2)
  1116. self.ae(str(s.visual_line(0)), '1')
  1117. self.ae(fco(), '0x\n1x\n')
  1118. # test: find downwards
  1119. s.scroll(2, False)
  1120. self.ae(str(s.visual_line(0)), '$ 1')
  1121. self.ae(fco(), '0x\n1x\n')
  1122. # resize
  1123. # get last cmd output with continued output mark
  1124. draw_prompt('3'), draw_output(1, 'long_line'), draw_output(2, 'l', False)
  1125. s.resize(4, 5)
  1126. s.scroll_to_prompt(-4)
  1127. self.ae(str(s.visual_line(0)), '$ 0')
  1128. self.ae(lco(), '0long_line\n0l\n1l\n')
  1129. # last visited cmd output
  1130. self.ae(lvco(), '0\n1\n2')
  1131. s.scroll_to_prompt(1)
  1132. self.ae(lvco(), '0x\n1x')
  1133. # last command output without line break
  1134. s = self.create_screen(cols=10, lines=3)
  1135. draw_prompt('p1')
  1136. mark_output(), s.draw('running')
  1137. self.ae(lco(), 'running')
  1138. s.index(), s.carriage_return()
  1139. self.ae(lco(), 'running\n')
  1140. # last command output from pager history
  1141. s = self.create_screen()
  1142. draw_prompt('p1')
  1143. draw_output(30)
  1144. self.ae(tuple(map(int, lco().split())), tuple(range(0, 30)))
  1145. # last non empty command output
  1146. s = self.create_screen()
  1147. draw_prompt('a'), draw_output(2, 'a')
  1148. draw_prompt('b'), mark_output()
  1149. self.ae(lco(), '')
  1150. self.ae(lco(which=3), '0a\n1a')
  1151. s.draw('running'), s.index(), s.carriage_return()
  1152. self.ae(lco(which=3), 'running\n')
  1153. s = self.create_screen()
  1154. draw_prompt('p1')
  1155. draw_output(30)
  1156. self.ae(tuple(map(int, lco(which=3).split())), tuple(range(0, 30)))
  1157. s = self.create_screen()
  1158. draw_prompt('p1')
  1159. draw_output(2, 'a')
  1160. draw_prompt('p1')
  1161. draw_prompt('p1')
  1162. self.ae(lco(which=3), '0a\n1a')
  1163. def test_pointer_shapes(self):
  1164. from kitty.window import set_pointer_shape
  1165. s = self.create_screen()
  1166. c = s.callbacks
  1167. response = ''
  1168. def cb(data):
  1169. nonlocal response
  1170. response = set_pointer_shape(s, str(data, 'utf-8'))
  1171. c.set_pointer_shape = cb
  1172. def send(a):
  1173. nonlocal response
  1174. response = ''
  1175. parse_bytes(s, f'\x1b]22;{a}\x1b\\'.encode())
  1176. return response
  1177. self.ae(send('?__current__'), '0')
  1178. self.ae(send('?__default__,__grabbed__,default,ne-resize,crosshair,XXX'), 'text,default,1,1,1,0')
  1179. def t(q, e=None):
  1180. self.ae(send(q), '')
  1181. self.ae(send('?__current__'), e)
  1182. t('default', 'default')
  1183. s.reset()
  1184. self.ae(send('?__current__'), '0')
  1185. t('=crosshair', 'crosshair')
  1186. t('<', '0')
  1187. t('=crosshair', 'crosshair')
  1188. t('', '0')
  1189. t('>help', 'help')
  1190. t('>wait', 'wait')
  1191. t('<', 'help')
  1192. t('<', '0')
  1193. t('default,help', 'help')
  1194. t('<', '0')
  1195. t('>default,help', 'help')
  1196. t('<', 'default')
  1197. t('<', '0')
  1198. t('=left_ptr', 'default')
  1199. t('=fleur', 'move')
  1200. def test_color_profile(self):
  1201. c = ColorProfile(defaults)
  1202. for i in range(8):
  1203. col = getattr(defaults, f'color{i}')
  1204. self.ae(c.as_color(i << 8 | 1), col)
  1205. self.ae(c.as_color(255 << 8 | 1), Color(0xee, 0xee, 0xee))
  1206. s = self.create_screen()
  1207. s.color_profile.reload_from_opts(defaults)
  1208. def q(send, expected=None):
  1209. s.callbacks.clear()
  1210. parse_bytes(s, b'\x1b]21;' + ';'.join(f'{k}={v}' for k, v in send.items()).encode() + b'\a')
  1211. self.ae(s.callbacks.color_control_responses, [expected] if expected else [])
  1212. q({k: '?' for k in 'background foreground 213 unknown'.split()}, {
  1213. 'background': defaults.background, 'foreground': defaults.foreground, '213': defaults.color213, 'unknown': '?'})
  1214. q({'background':'aquamarine'})
  1215. q({'background':'?', 'selection_background': '?'}, {'background': color_names['aquamarine'], 'selection_background': s.color_profile.highlight_bg})
  1216. q({'selection_background': ''})
  1217. self.assertIsNone(s.color_profile.highlight_bg)
  1218. q({'selection_background': '?'}, {'selection_background': ''})
  1219. s.color_profile.reload_from_opts(defaults)
  1220. q({'transparent_background_color9': '?'}, {'transparent_background_color9': '?'})
  1221. q({'transparent_background_color2': '?'}, {'transparent_background_color2': ''})
  1222. q({'transparent_background_color2': 'red@0.5'})
  1223. q({'transparent_background_color2': '?'}, {'transparent_background_color2': (Color(255, 0, 0), 126)})
  1224. q({'transparent_background_color2': '#ffffff@-1'})
  1225. q({'transparent_background_color2': '?'}, {'transparent_background_color2': (Color(255, 255, 255), 255)})