screen.py 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290
  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. s = self.create_screen(scrollback=6)
  251. s.draw(''.join([str(i) * s.columns for i in range(s.lines)]))
  252. s.resize(3, 10)
  253. self.ae(str(s.line(0)), '0'*5 + '1'*5)
  254. self.ae(str(s.line(1)), '2'*5 + '3'*5)
  255. self.ae(str(s.line(2)), '4'*5)
  256. s.resize(5, 1)
  257. self.ae(str(s.line(0)), '4')
  258. self.ae(str(s.historybuf), '3\n3\n3\n3\n3\n2')
  259. s = self.create_screen(scrollback=20)
  260. s.draw(''.join(str(i) * s.columns for i in range(s.lines*2)))
  261. self.ae(str(s.linebuf), '55555\n66666\n77777\n88888\n99999')
  262. s.resize(5, 2)
  263. self.ae(str(s.linebuf), '88\n88\n99\n99\n9')
  264. s = self.create_screen()
  265. s.draw('a' * s.columns)
  266. s.linefeed(), s.carriage_return()
  267. s.draw('bb')
  268. s.resize(s.lines, s.columns - 2)
  269. self.ae(str(s.linebuf), 'aaa\naa\nbb\n\n')
  270. s.cursor.y = s.cursor.x = 0
  271. s.draw('x' * len(str(s.line(0))))
  272. s.linefeed(), s.carriage_return()
  273. s.draw('x' * len(str(s.line(1))))
  274. s.resize(s.lines, s.columns + 4)
  275. self.ae(str(s.linebuf), 'xxx\nxx\nbb\n\n')
  276. s = self.create_screen()
  277. c = s.callbacks
  278. parse_bytes(s, b'\x1b[?2048$p') # ]
  279. self.ae(c.wtcbuf, b'\x1b[?2048;2$y') # ]
  280. c.clear()
  281. parse_bytes(s, b'\x1b[?2048h\x1b[?2048$p') # ]]
  282. self.ae(c.wtcbuf, b'\x1b[?2048;1$y') # ]
  283. self.ae(c.num_of_resize_events, 1)
  284. parse_bytes(s, b'\x1b[?2048h') # ]
  285. self.ae(c.num_of_resize_events, 2)
  286. def test_cursor_after_resize(self):
  287. def draw(text, end_line=True):
  288. s.draw(text)
  289. if end_line:
  290. s.linefeed(), s.carriage_return()
  291. s = self.create_screen()
  292. draw('123'), draw('123')
  293. y_before = s.cursor.y
  294. s.resize(s.lines, s.columns-1)
  295. self.ae(y_before, s.cursor.y)
  296. s = self.create_screen(cols=5, lines=8)
  297. draw('one')
  298. draw('two three four five |||', end_line=False)
  299. s.resize(s.lines + 2, s.columns + 2)
  300. y = s.cursor.y
  301. self.assertIn('|', str(s.line(y)))
  302. s = self.create_screen()
  303. draw('a')
  304. x_before = s.cursor.x
  305. s.resize(s.lines - 1, s.columns)
  306. self.ae(x_before, s.cursor.x)
  307. s = self.create_screen()
  308. s.draw('abc')
  309. b = s.cursor.x
  310. s.resize(7, s.columns)
  311. self.assertEqual(s.cursor.x, b)
  312. s.cursor.x = 0
  313. s.resize(5, s.columns)
  314. self.assertEqual(s.cursor.x, 0)
  315. def test_scrollback_fill_after_resize(self):
  316. def prepare_screen(content=()):
  317. ans = self.create_screen(options={'scrollback_fill_enlarged_window': True})
  318. for line in content:
  319. ans.draw(line)
  320. ans.linefeed()
  321. ans.carriage_return()
  322. return ans
  323. def assert_lines(*lines):
  324. return self.ae(lines, tuple(str(s.line(i)) for i in range(s.lines)))
  325. # test the reverse scroll function
  326. s = prepare_screen(map(str, range(6)))
  327. assert_lines('2', '3', '4', '5', '')
  328. s.reverse_scroll(2, True)
  329. assert_lines('0', '1', '2', '3', '4')
  330. # Height increased, width unchanged → pull down lines to fill new space at the top
  331. s = prepare_screen(map(str, range(6)))
  332. assert_lines('2', '3', '4', '5', '')
  333. dist_from_bottom = s.lines - s.cursor.y
  334. s.resize(7, s.columns)
  335. assert_lines('0', '1', '2', '3', '4', '5', '')
  336. self.ae(dist_from_bottom, s.lines - s.cursor.y)
  337. # Height increased, width increased → rewrap, pull down
  338. s = prepare_screen(['0', '1', '2', '3' * 15])
  339. assert_lines('2', '33333', '33333', '33333', '')
  340. s.resize(7, 12)
  341. assert_lines('0', '1', '2', '333333333333', '333', '', '')
  342. # Height increased, width decreased → rewrap, pull down if possible
  343. s = prepare_screen(['0', '1', '2', '3' * 5])
  344. assert_lines('0', '1', '2', '33333', '')
  345. s.resize(6, 4)
  346. assert_lines('0', '1', '2', '3333', '3', '')
  347. # Height unchanged, width increased → rewrap, pull down if possible
  348. s = prepare_screen(['0', '1', '2', '3' * 15])
  349. assert_lines('2', '33333', '33333', '33333', '')
  350. s.resize(s.lines, 12)
  351. assert_lines('1', '2', '333333333333', '333', '')
  352. # Height decreased, width increased → rewrap, pull down if possible
  353. s = prepare_screen(['0', '1', '2', '3' * 15])
  354. assert_lines('2', '33333', '33333', '33333', '')
  355. s.resize(4, 12)
  356. assert_lines('2', '333333333333', '333', '')
  357. # Height increased with large continued text
  358. s = self.create_screen(options={'scrollback_fill_enlarged_window': True})
  359. s.draw(('x' * (s.columns * s.lines * 2)) + 'abcde')
  360. s.carriage_return(), s.linefeed()
  361. s.draw('>')
  362. assert_lines('xxxxx', 'xxxxx', 'xxxxx', 'abcde', '>')
  363. s.resize(s.lines + 2, s.columns)
  364. assert_lines('xxxxx', 'xxxxx', 'xxxxx', 'xxxxx', 'xxxxx', 'abcde', '>')
  365. def test_tab_stops(self):
  366. # Taken from vttest/main.c
  367. s = self.create_screen(cols=80, lines=2)
  368. s.cursor_position(1, 1)
  369. s.clear_tab_stop(3)
  370. for col in range(1, s.columns - 1, 3):
  371. s.cursor_forward(3)
  372. s.set_tab_stop()
  373. s.cursor_position(1, 4)
  374. for col in range(4, s.columns - 1, 6):
  375. s.clear_tab_stop(0)
  376. s.cursor_forward(6)
  377. s.cursor_position(1, 7)
  378. s.clear_tab_stop(2)
  379. s.cursor_position(1, 1)
  380. for col in range(1, s.columns - 1, 6):
  381. s.tab()
  382. s.draw('*')
  383. s.cursor_position(2, 2)
  384. self.ae(str(s.line(0)), '\t*'*13)
  385. s = self.create_screen(cols=4, lines=2)
  386. s.draw('aaaX\tbbbb')
  387. self.ae(str(s.line(0)) + str(s.line(1)), 'aaaXbbbb')
  388. def test_margins(self):
  389. # Taken from vttest/main.c
  390. s = self.create_screen(cols=80, lines=24)
  391. def nl():
  392. s.carriage_return(), s.linefeed()
  393. for deccolm in (False, True):
  394. if deccolm:
  395. s.resize(24, 132)
  396. s.set_mode(DECCOLM)
  397. else:
  398. s.reset_mode(DECCOLM)
  399. region = s.lines - 6
  400. s.set_margins(3, region + 3)
  401. s.set_mode(DECOM)
  402. for i in range(26):
  403. ch = chr(ord('A') + i)
  404. which = i % 4
  405. if which == 0:
  406. s.cursor_position(region + 1, 1), s.draw(ch)
  407. s.cursor_position(region + 1, s.columns), s.draw(ch.lower())
  408. nl()
  409. elif which == 1:
  410. # Simple wrapping
  411. s.cursor_position(region, s.columns), s.draw(chr(ord('A') + i - 1).lower() + ch)
  412. # Backspace at right margin
  413. s.cursor_position(region + 1, s.columns), s.draw(ch), s.backspace(), s.draw(ch.lower())
  414. nl()
  415. elif which == 2:
  416. # Tab to right margin
  417. s.cursor_position(region + 1, s.columns), s.draw(ch), s.backspace(), s.backspace(), s.tab(), s.tab(), s.draw(ch.lower())
  418. s.cursor_position(region + 1, 2), s.backspace(), s.draw(ch), nl()
  419. else:
  420. s.cursor_position(region + 1, 1), nl()
  421. s.cursor_position(region, 1), s.draw(ch)
  422. s.cursor_position(region, s.columns), s.draw(ch.lower())
  423. for ln in range(2, region + 2):
  424. c = chr(ord('I') + ln - 2)
  425. before = '\t' if ln % 4 == 0 else ' '
  426. self.ae(c + ' ' * (s.columns - 3) + before + c.lower(), str(s.line(ln)))
  427. s.reset_mode(DECOM)
  428. # Test that moving cursor outside the margins works as expected
  429. s = self.create_screen(10, 10)
  430. s.set_margins(4, 6)
  431. s.cursor_position(0, 0)
  432. self.ae(s.cursor.y, 0)
  433. nl()
  434. self.ae(s.cursor.y, 1)
  435. s.cursor.y = s.lines - 1
  436. self.ae(s.cursor.y, 9)
  437. s.reverse_index()
  438. self.ae(s.cursor.y, 8)
  439. def test_sgr(self):
  440. s = self.create_screen()
  441. s.select_graphic_rendition(0, 1, 37, 42)
  442. s.draw('a')
  443. c = s.line(0).cursor_from(0)
  444. self.assertTrue(c.bold)
  445. self.ae(c.bg, (2 << 8) | 1)
  446. s.cursor_position(2, 1)
  447. s.select_graphic_rendition(0, 35)
  448. s.draw('b')
  449. c = s.line(1).cursor_from(0)
  450. self.ae(c.fg, (5 << 8) | 1)
  451. self.ae(c.bg, 0)
  452. s.cursor_position(2, 2)
  453. s.select_graphic_rendition(38, 2, 99, 1, 2, 3)
  454. s.draw('c')
  455. c = s.line(1).cursor_from(1)
  456. self.ae(c.fg, (1 << 24) | (2 << 16) | (3 << 8) | 2)
  457. def test_cursor_hidden(self):
  458. s = self.create_screen()
  459. s.toggle_alt_screen()
  460. s.cursor_visible = False
  461. s.toggle_alt_screen()
  462. self.assertFalse(s.cursor_visible)
  463. def test_dirty_lines(self):
  464. s = self.create_screen()
  465. self.assertFalse(s.linebuf.dirty_lines())
  466. s.draw('a' * (s.columns * 2))
  467. self.ae(s.linebuf.dirty_lines(), [0, 1])
  468. self.assertFalse(s.historybuf.dirty_lines())
  469. while not s.historybuf.count:
  470. s.draw('a' * (s.columns * 2))
  471. self.ae(s.historybuf.dirty_lines(), list(range(s.historybuf.count)))
  472. self.ae(s.linebuf.dirty_lines(), list(range(s.lines)))
  473. s.cursor.x, s.cursor.y = 0, 1
  474. s.insert_lines(2)
  475. self.ae(s.linebuf.dirty_lines(), [0, 3, 4])
  476. s.draw('a' * (s.columns * s.lines))
  477. self.ae(s.linebuf.dirty_lines(), list(range(s.lines)))
  478. s.cursor.x, s.cursor.y = 0, 1
  479. s.delete_lines(2)
  480. self.ae(s.linebuf.dirty_lines(), [0, 1, 2])
  481. s = self.create_screen()
  482. self.assertFalse(s.linebuf.dirty_lines())
  483. s.erase_in_line(0, False)
  484. self.ae(s.linebuf.dirty_lines(), [0])
  485. s.index(), s.index()
  486. s.erase_in_display(0, False)
  487. self.ae(s.linebuf.dirty_lines(), [0, 2, 3, 4])
  488. s = self.create_screen()
  489. self.assertFalse(s.linebuf.dirty_lines())
  490. s.insert_characters(2)
  491. self.ae(s.linebuf.dirty_lines(), [0])
  492. s.cursor.y = 1
  493. s.delete_characters(2)
  494. self.ae(s.linebuf.dirty_lines(), [0, 1])
  495. s.cursor.y = 2
  496. s.erase_characters(2)
  497. self.ae(s.linebuf.dirty_lines(), [0, 1, 2])
  498. def test_selection_as_text(self):
  499. s = self.create_screen()
  500. for i in range(2 * s.lines):
  501. if i != 0:
  502. s.carriage_return(), s.linefeed()
  503. s.draw(str(i) * s.columns)
  504. s.start_selection(0, 0)
  505. s.update_selection(4, 4)
  506. def ts(*args):
  507. return ''.join(s.text_for_selection(*args))
  508. expected = ''.join(('55555', '\n66666', '\n77777', '\n88888', '\n99999'))
  509. self.ae(ts(), expected)
  510. s.scroll(2, True)
  511. self.ae(ts(), expected)
  512. s.reset()
  513. s.draw('ab cd')
  514. s.start_selection(0, 0)
  515. s.update_selection(1, 3)
  516. self.ae(ts(), ''.join(('ab ', 'cd')))
  517. self.ae(ts(False, True), ''.join(('ab', 'cd')))
  518. s.reset()
  519. s.draw('ab cd')
  520. s.start_selection(0, 0)
  521. s.update_selection(3, 4)
  522. self.ae(s.text_for_selection(), ('ab ', ' ', 'cd'))
  523. self.ae(s.text_for_selection(False, True), ('ab', '\n', 'cd'))
  524. s.reset()
  525. s.draw('a')
  526. s.select_graphic_rendition(32)
  527. s.draw('b')
  528. s.select_graphic_rendition(39)
  529. s.draw('c xy')
  530. s.start_selection(0, 0)
  531. s.update_selection(1, 3)
  532. self.ae(s.text_for_selection(), ('abc ', 'xy'))
  533. self.ae(s.text_for_selection(True), ('a\x1b[32mb\x1b[39mc ', 'xy', '\x1b[m'))
  534. self.ae(s.text_for_selection(True, True), ('a\x1b[32mb\x1b[39mc', 'xy', '\x1b[m'))
  535. def test_soft_hyphen(self):
  536. s = self.create_screen()
  537. s.draw('a\u00adb')
  538. self.ae(s.cursor.x, 2)
  539. s.start_selection(0, 0)
  540. s.update_selection(2, 0)
  541. self.ae(s.text_for_selection(), ('a\u00adb',))
  542. def test_variation_selectors(self):
  543. s = self.create_screen()
  544. s.draw('\U0001f610')
  545. self.ae(s.cursor.x, 2)
  546. s.carriage_return(), s.linefeed()
  547. s.draw('\U0001f610\ufe0e')
  548. self.ae(s.cursor.x, 1)
  549. s.carriage_return(), s.linefeed()
  550. s.draw('\u25b6')
  551. self.ae(s.cursor.x, 1)
  552. s.carriage_return(), s.linefeed()
  553. s.draw('\u25b6\ufe0f')
  554. self.ae(s.cursor.x, 2)
  555. def test_writing_with_cursor_on_trailer_of_wide_character(self):
  556. s = self.create_screen()
  557. def r(x, pos, expected):
  558. s.reset()
  559. s.draw('😸')
  560. self.ae(s.cursor.x, 2)
  561. s.cursor.x = 1
  562. s.draw(x)
  563. self.ae(s.cursor.x, pos)
  564. self.ae(str(s.line(0)), expected)
  565. r('a', 2, ' a')
  566. r('😸', 3, ' 😸')
  567. r('\u0304', 1, '😸\u0304')
  568. r('\r', 0, '😸')
  569. def test_serialize(self):
  570. from kitty.window import as_text
  571. s = self.create_screen()
  572. parse_bytes(s, b'\x1b[1;91m')
  573. s.draw('X')
  574. parse_bytes(s, b'\x1b[0m\x1b[2m')
  575. s.draw('Y')
  576. self.ae(as_text(s, True), '\x1b[m\x1b[22;1;91mX\x1b[22;2;39mY\n\n\n\n')
  577. s.reset()
  578. s.draw('ab' * s.columns)
  579. s.carriage_return(), s.linefeed()
  580. s.draw('c')
  581. self.ae(as_text(s), 'ababababab\nc\n\n')
  582. self.ae(as_text(s, True), '\x1b[mababa\x1b[mbabab\n\x1b[mc\n\n')
  583. s = self.create_screen(cols=2, lines=2, scrollback=2)
  584. for i in range(1, 7):
  585. s.select_graphic_rendition(30 + i)
  586. s.draw(f'{i}' * s.columns)
  587. 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')
  588. def set_link(url=None, id=None):
  589. parse_bytes(s, '\x1b]8;id={};{}\x1b\\'.format(id or '', url or '').encode('utf-8'))
  590. s = self.create_screen()
  591. s.draw('a')
  592. set_link('moo', 'foo')
  593. s.draw('bcdef')
  594. self.ae(as_text(s, True), '\x1b[ma\x1b]8;id=foo;moo\x1b\\bcde\x1b[mf\n\n\n\x1b]8;;\x1b\\')
  595. set_link()
  596. s.draw('gh')
  597. self.ae(as_text(s, True), '\x1b[ma\x1b]8;id=foo;moo\x1b\\bcde\x1b[mf\x1b]8;;\x1b\\gh\n\n\n')
  598. s = self.create_screen()
  599. s.draw('a')
  600. set_link('moo')
  601. s.draw('bcdef')
  602. self.ae(as_text(s, True), '\x1b[ma\x1b]8;;moo\x1b\\bcde\x1b[mf\n\n\n\x1b]8;;\x1b\\')
  603. def test_wrapping_serialization(self):
  604. from kitty.window import as_text
  605. s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128})
  606. s.draw('aabbccddeeff')
  607. self.ae(as_text(s, add_history=True), 'aabbccddeeff')
  608. self.assertNotIn('\n', as_text(s, add_history=True, as_ansi=True))
  609. s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128})
  610. s.draw('1'), s.carriage_return(), s.linefeed()
  611. s.draw('2'), s.carriage_return(), s.linefeed()
  612. s.draw('3'), s.carriage_return(), s.linefeed()
  613. s.draw('4'), s.carriage_return(), s.linefeed()
  614. s.draw('5'), s.carriage_return(), s.linefeed()
  615. s.draw('6'), s.carriage_return(), s.linefeed()
  616. s.draw('7')
  617. self.ae(as_text(s, add_history=True), '1\n2\n3\n4\n5\n6\n7')
  618. s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128})
  619. s.draw('aabb')
  620. s.cursor.y = 0
  621. s.carriage_return(), s.linefeed()
  622. self.ae(as_text(s, add_history=True), 'aabb')
  623. s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128})
  624. s.draw('a'), s.carriage_return(), s.linefeed()
  625. s.cursor.y = 0
  626. s.draw('aabb')
  627. self.ae(as_text(s), 'aabb')
  628. s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': 128})
  629. s.draw('a😀')
  630. self.ae(as_text(s), 'a😀')
  631. def test_pagerhist(self):
  632. hsz = 8
  633. s = self.create_screen(cols=2, lines=2, scrollback=2, options={'scrollback_pager_history_size': hsz})
  634. def contents():
  635. return s.historybuf.pagerhist_as_text()
  636. def line(i):
  637. q.append('\x1b[m' + f'{i}' * s.columns + '\r')
  638. def w(x):
  639. s.historybuf.pagerhist_write(x)
  640. def test():
  641. expected = ''.join(q)
  642. maxlen = hsz
  643. extra = len(expected) - maxlen
  644. if extra > 0:
  645. expected = expected[extra:]
  646. got = contents()
  647. self.ae(got, expected)
  648. q = []
  649. for i in range(4):
  650. s.draw(f'{i}' * s.columns)
  651. self.ae(contents(), '')
  652. s.draw('4' * s.columns), line(0), test()
  653. s.draw('5' * s.columns), line(1), test()
  654. s.draw('6' * s.columns), line(2), test()
  655. s.draw('7' * s.columns), line(3), test()
  656. s.draw('8' * s.columns), line(4), test()
  657. s.draw('9' * s.columns), line(5), test()
  658. s = self.create_screen(options={'scrollback_pager_history_size': 2048})
  659. text = '\x1b[msoft\r\x1b[mbreak\nnext😼cat'
  660. w(text)
  661. self.ae(contents(), text)
  662. s.historybuf.pagerhist_rewrap(2)
  663. self.ae(contents(), '\x1b[mso\rft\x1b[m\rbr\rea\rk\nne\rxt\r😼\rca\rt')
  664. s = self.create_screen(options={'scrollback_pager_history_size': 8})
  665. w('😼')
  666. self.ae(contents(), '😼')
  667. w('abcd')
  668. self.ae(contents(), '😼abcd')
  669. w('e')
  670. self.ae(contents(), 'abcde')
  671. def test_user_marking(self):
  672. def cells(*a, y=0, mark=3):
  673. return [(x, y, mark) for x in a]
  674. s = self.create_screen()
  675. s.draw('abaa')
  676. s.carriage_return(), s.linefeed()
  677. s.draw('xyxyx')
  678. s.set_marker(marker_from_regex('a', 3))
  679. self.ae(s.marked_cells(), cells(0, 2, 3))
  680. s.set_marker()
  681. self.ae(s.marked_cells(), [])
  682. def mark_x(text):
  683. col = 0
  684. for i, c in enumerate(text):
  685. if c == 'x':
  686. col += 1
  687. yield i, i, col
  688. s.set_marker(marker_from_function(mark_x))
  689. self.ae(s.marked_cells(), [(0, 1, 1), (2, 1, 2), (4, 1, 3)])
  690. s = self.create_screen(lines=5, scrollback=10)
  691. for i in range(15):
  692. s.draw(str(i))
  693. if i != 14:
  694. s.carriage_return(), s.linefeed()
  695. s.set_marker(marker_from_regex(r'\d+', 3))
  696. for i in range(10):
  697. self.assertTrue(s.scroll_to_next_mark())
  698. self.ae(s.scrolled_by, i + 1)
  699. self.ae(s.scrolled_by, 10)
  700. for i in range(10):
  701. self.assertTrue(s.scroll_to_next_mark(0, False))
  702. self.ae(s.scrolled_by, 10 - i - 1)
  703. self.ae(s.scrolled_by, 0)
  704. s = self.create_screen()
  705. s.draw('🐈ab')
  706. s.set_marker(marker_from_regex('🐈', 3))
  707. self.ae(s.marked_cells(), cells(0, 1))
  708. s.set_marker(marker_from_regex('🐈a', 3))
  709. self.ae(s.marked_cells(), cells(0, 1, 2))
  710. s.set_marker(marker_from_regex('a', 3))
  711. self.ae(s.marked_cells(), cells(2))
  712. s = self.create_screen(cols=20)
  713. s.tab()
  714. s.draw('ab')
  715. s.set_marker(marker_from_regex('a', 3))
  716. self.ae(s.marked_cells(), cells(8))
  717. s.set_marker(marker_from_regex('\t', 3))
  718. self.ae(s.marked_cells(), cells(*range(8)))
  719. s = self.create_screen()
  720. s.cursor.x = 2
  721. s.draw('x')
  722. s.cursor.x += 1
  723. s.draw('x')
  724. s.set_marker(marker_from_function(mark_x))
  725. self.ae(s.marked_cells(), [(2, 0, 1), (4, 0, 2)])
  726. def test_hyperlinks(self):
  727. s = self.create_screen()
  728. self.ae(s.line(0).hyperlink_ids(), tuple(0 for x in range(s.columns)))
  729. def set_link(url=None, id=None):
  730. parse_bytes(s, '\x1b]8;id={};{}\x1b\\'.format(id or '', url or '').encode('utf-8'))
  731. set_link('url-a', 'a')
  732. self.ae(s.line(0).hyperlink_ids(), tuple(0 for x in range(s.columns)))
  733. s.draw('a')
  734. self.ae(s.line(0).hyperlink_ids(), (1,) + tuple(0 for x in range(s.columns - 1)))
  735. s.draw('bc')
  736. self.ae(s.line(0).hyperlink_ids(), (1, 1, 1, 0, 0))
  737. set_link()
  738. s.draw('d')
  739. self.ae(s.line(0).hyperlink_ids(), (1, 1, 1, 0, 0))
  740. set_link('url-a', 'a')
  741. s.draw('efg')
  742. self.ae(s.line(0).hyperlink_ids(), (1, 1, 1, 0, 1))
  743. self.ae(s.line(1).hyperlink_ids(), (1, 1, 0, 0, 0))
  744. set_link('url-b')
  745. s.draw('hij')
  746. self.ae(s.line(1).hyperlink_ids(), (1, 1, 2, 2, 2))
  747. set_link()
  748. self.ae({('a:url-a', 1), (':url-b', 2)}, s.hyperlinks_as_set())
  749. s.garbage_collect_hyperlink_pool()
  750. self.ae({('a:url-a', 1), (':url-b', 2)}, s.hyperlinks_as_set())
  751. for i in range(s.lines + 2):
  752. s.linefeed()
  753. s.garbage_collect_hyperlink_pool()
  754. self.ae({('a:url-a', 1), (':url-b', 2)}, s.hyperlinks_as_set())
  755. for i in range(s.lines * 2):
  756. s.linefeed()
  757. s.garbage_collect_hyperlink_pool()
  758. self.assertFalse(s.hyperlinks_as_set())
  759. set_link('url-a', 'x')
  760. s.draw('a')
  761. set_link('url-a', 'y')
  762. s.draw('a')
  763. set_link()
  764. self.ae({('x:url-a', 1), ('y:url-a', 2)}, s.hyperlinks_as_set())
  765. s = self.create_screen()
  766. set_link('u' * 2048)
  767. s.draw('a')
  768. self.ae({(':' + 'u' * 2045, 1)}, s.hyperlinks_as_set())
  769. s = self.create_screen()
  770. set_link('u' * 2048, 'i' * 300)
  771. s.draw('a')
  772. self.ae({('i'*256 + ':' + 'u' * (2045 - 256), 1)}, s.hyperlinks_as_set())
  773. s = self.create_screen()
  774. set_link('1'), s.draw('1')
  775. set_link('2'), s.draw('2')
  776. set_link('3'), s.draw('3')
  777. s.cursor.x = 1
  778. set_link(), s.draw('X')
  779. self.ae(s.line(0).hyperlink_ids(), (1, 0, 3, 0, 0))
  780. self.ae({(':1', 1), (':2', 2), (':3', 3)}, s.hyperlinks_as_set())
  781. s.garbage_collect_hyperlink_pool()
  782. self.ae({(':1', 1), (':3', 2)}, s.hyperlinks_as_set())
  783. set_link('3'), s.draw('3')
  784. self.ae({(':1', 1), (':3', 2)}, s.hyperlinks_as_set())
  785. set_link('4'), s.draw('4')
  786. self.ae({(':1', 1), (':3', 2), (':4', 3)}, s.hyperlinks_as_set())
  787. s = self.create_screen()
  788. set_link('1'), s.draw('1')
  789. set_link('2'), s.draw('2')
  790. set_link('1'), s.draw('1')
  791. self.ae({(':2', 2), (':1', 1)}, s.hyperlinks_as_set())
  792. s = self.create_screen()
  793. set_link('1'), s.draw('12'), set_link(), s.draw('X'), set_link('1'), s.draw('3')
  794. s.linefeed(), s.carriage_return()
  795. s.draw('abc')
  796. s.linefeed(), s.carriage_return()
  797. set_link(), s.draw('Z ')
  798. set_link('1'), s.draw('xyz')
  799. s.linefeed(), s.carriage_return()
  800. set_link('2'), s.draw('Z Z')
  801. self.assertIsNone(s.current_url_text())
  802. self.assertIsNone(s.hyperlink_at(0, 4))
  803. self.assertIsNone(s.current_url_text())
  804. self.ae(s.hyperlink_at(0, 0), '1')
  805. self.ae(s.current_url_text(), '123abcxyz')
  806. self.ae('1', s.hyperlink_at(3, 2))
  807. self.ae(s.current_url_text(), '123abcxyz')
  808. self.ae('2', s.hyperlink_at(1, 3))
  809. self.ae(s.current_url_text(), 'Z Z')
  810. def test_bottom_margin(self):
  811. s = self.create_screen(cols=80, lines=6, scrollback=4)
  812. s.set_margins(0, 5)
  813. for i in range(8):
  814. s.draw(str(i))
  815. s.linefeed()
  816. s.carriage_return()
  817. self.ae(str(s.linebuf), '4\n5\n6\n7\n\n')
  818. self.ae(str(s.historybuf), '3\n2\n1\n0')
  819. def test_top_margin(self):
  820. s = self.create_screen(cols=80, lines=6, scrollback=4)
  821. s.set_margins(2, 6)
  822. for i in range(8):
  823. s.draw(str(i))
  824. s.linefeed()
  825. s.carriage_return()
  826. self.ae(str(s.linebuf), '0\n4\n5\n6\n7\n')
  827. self.ae(str(s.historybuf), '')
  828. def test_top_and_bottom_margin(self):
  829. s = self.create_screen(cols=80, lines=6, scrollback=4)
  830. s.set_margins(2, 5)
  831. for i in range(8):
  832. s.draw(str(i))
  833. s.linefeed()
  834. s.carriage_return()
  835. self.ae(str(s.linebuf), '0\n5\n6\n7\n\n')
  836. self.ae(str(s.historybuf), '')
  837. def test_osc_52(self):
  838. s = self.create_screen()
  839. c = s.callbacks
  840. def send(what: str):
  841. return parse_bytes(s, f'\033]52;p;{what}\a'.encode('ascii'))
  842. def t(q, *expected):
  843. c.clear()
  844. send(q)
  845. del q
  846. t.ex = list(expected)
  847. del expected
  848. try:
  849. self.ae(tuple(map(len, c.cc_buf)), tuple(map(len, t.ex)))
  850. self.ae(c.cc_buf, t.ex)
  851. finally:
  852. del t.ex
  853. t('XYZ', ('p;XYZ', False))
  854. t('a' * VT_PARSER_BUFFER_SIZE, ('p;' + 'a' * (VT_PARSER_BUFFER_SIZE - 8), True), (';' + 'a' * 8, False))
  855. t('', ('p;', False))
  856. t('!', ('p;!', False))
  857. def test_key_encoding_flags_stack(self):
  858. s = self.create_screen()
  859. c = s.callbacks
  860. def w(code, p1='', p2=''):
  861. p = f'{p1}'
  862. if p2:
  863. p += f';{p2}'
  864. return parse_bytes(s, f'\033[{code}{p}u'.encode('ascii'))
  865. def ac(flags):
  866. parse_bytes(s, b'\033[?u')
  867. self.ae(c.wtcbuf, f'\033[?{flags}u'.encode('ascii'))
  868. c.clear()
  869. ac(0)
  870. w('=', 0b1001)
  871. ac(0b1001)
  872. w('=', 0b0011, 2)
  873. ac(0b1011)
  874. w('=', 0b0110, 3)
  875. ac(0b1001)
  876. s.reset()
  877. ac(0)
  878. w('>', 0b0011)
  879. ac(0b0011)
  880. w('=', 0b1111)
  881. ac(0b1111)
  882. w('>', 0b10)
  883. ac(0b10)
  884. w('<')
  885. ac(0b1111)
  886. for i in range(10):
  887. w('<')
  888. ac(0)
  889. s.reset()
  890. for i in range(1, 16):
  891. w('>', i)
  892. ac(15)
  893. w('<'), ac(14), w('<'), ac(13)
  894. def test_color_stack(self):
  895. s = self.create_screen()
  896. c = s.callbacks
  897. def w(code):
  898. return parse_bytes(s, ('\033[' + code).encode('ascii'))
  899. def ac(idx, count):
  900. self.ae(c.wtcbuf, f'\033[{idx};{count}#Q'.encode('ascii'))
  901. c.clear()
  902. w('#R')
  903. ac(0, 0)
  904. w('#P')
  905. w('#R')
  906. ac(0, 1)
  907. w('10#P')
  908. w('#R')
  909. ac(0, 1)
  910. w('#Q')
  911. w('#R')
  912. ac(0, 0)
  913. for i in range(20):
  914. w('#P')
  915. w('#R')
  916. ac(9, 10)
  917. def test_detect_url(self):
  918. s = self.create_screen(cols=30)
  919. def ae(expected, x=3, y=0):
  920. s.detect_url(x, y)
  921. url = ''.join(s.text_for_marked_url())
  922. self.assertEqual(expected, url)
  923. def t(url, x=0, y=0, before='', after='', expected=''):
  924. s.reset()
  925. s.cursor.x = x
  926. s.cursor.y = y
  927. s.draw(before + url + after)
  928. ae(expected or url, x=x + 1 + len(before), y=y)
  929. t('http://moo.com')
  930. t('http://moo.com/something?else=+&what-')
  931. t('http://moo.com#fragme')
  932. for (st, e) in '() {} [] <>'.split():
  933. t('http://moo.com', before=st, after=e)
  934. for trailer in ')-=':
  935. t('http://moo.com' + trailer)
  936. for trailer in '{}([<>':
  937. t('http://moo.com', after=trailer)
  938. t('http://moo.com', x=s.columns - 9)
  939. t('https://wraps-by-one-char.com', before='[', after=']')
  940. t('http://[::1]:8080')
  941. t('https://wr[aps-by-one-ch]ar.com')
  942. t('http://[::1]:8080/x', after='[')
  943. t('http://[::1]:8080/x]y34', expected='http://[::1]:8080/x')
  944. t('https://wraps-by-one-char.com[]/x', after='[')
  945. def test_prompt_marking(self):
  946. s = self.create_screen()
  947. def mark_prompt():
  948. parse_bytes(s, b'\033]133;A\007')
  949. def mark_output():
  950. parse_bytes(s, b'\033]133;C\007')
  951. for i in range(4):
  952. mark_prompt()
  953. s.draw(f'$ {i}')
  954. s.carriage_return()
  955. s.index(), s.index()
  956. self.ae(s.scrolled_by, 0)
  957. self.assertTrue(s.scroll_to_prompt())
  958. self.ae(str(s.visual_line(0)), '$ 1')
  959. self.assertTrue(s.scroll_to_prompt())
  960. self.ae(str(s.visual_line(0)), '$ 0')
  961. self.assertFalse(s.scroll_to_prompt())
  962. self.assertTrue(s.scroll_to_prompt(1))
  963. self.ae(str(s.visual_line(0)), '$ 1')
  964. self.assertTrue(s.scroll_to_prompt(1))
  965. self.ae(str(s.visual_line(0)), '$ 2')
  966. self.assertFalse(s.scroll_to_prompt(1))
  967. s = self.create_screen()
  968. mark_prompt(), s.draw('$ 0')
  969. s.carriage_return(), s.index()
  970. mark_prompt(), s.draw('$ 1')
  971. for i in range(s.lines):
  972. s.carriage_return(), s.index()
  973. s.draw(str(i))
  974. self.assertTrue(s.scroll_to_prompt())
  975. self.ae(str(s.visual_line(0)), '$ 1')
  976. def lco(as_ansi=False, which=0):
  977. a = []
  978. if s.cmd_output(which, a.append, as_ansi):
  979. pht = pagerhist(s, as_ansi=as_ansi, upto_output_start=True)
  980. if pht:
  981. a.insert(0, pht)
  982. return ''.join(a)
  983. def fco():
  984. a = []
  985. s.cmd_output(1, a.append)
  986. return ''.join(a)
  987. def lvco():
  988. a = []
  989. s.cmd_output(2, a.append)
  990. return ''.join(a)
  991. s = self.create_screen()
  992. s.draw('abcd'), s.index(), s.carriage_return()
  993. s.draw('12'), s.index(), s.carriage_return()
  994. self.ae(fco(), '')
  995. self.ae(lco(), 'abcd\n12\n')
  996. s = self.create_screen()
  997. mark_prompt(), s.draw('$ 0')
  998. s.carriage_return(), s.index()
  999. mark_output()
  1000. s.draw('abcd'), s.index(), s.carriage_return()
  1001. s.draw('12'), s.index(), s.carriage_return()
  1002. mark_prompt(), s.draw('$ 1')
  1003. self.ae(fco(), 'abcd\n12')
  1004. self.ae(lco(), 'abcd\n12')
  1005. self.ae(lco(as_ansi=True), '\x1b[m\x1b]133;C\x1b\\abcd\n\x1b[m12')
  1006. def draw_prompt(x):
  1007. mark_prompt(), s.draw(f'$ {x}'), s.carriage_return(), s.index()
  1008. def draw_output(n, x='', m=True):
  1009. if m:
  1010. mark_output()
  1011. for i in range(n):
  1012. s.draw(f'{i}{x}'), s.index(), s.carriage_return()
  1013. s = self.create_screen(cols=5, lines=5, scrollback=15)
  1014. draw_output(1, 'start', False)
  1015. draw_prompt('0'), draw_output(3)
  1016. draw_prompt('1')
  1017. draw_prompt('2'), draw_output(2, 'x')
  1018. # last cmd output
  1019. # test: find upwards
  1020. self.ae(s.scrolled_by, 0)
  1021. self.ae(lco(), '0x\n1x\n')
  1022. # get output after scroll up
  1023. s.scroll_to_prompt()
  1024. self.ae(s.scrolled_by, 4)
  1025. self.ae(str(s.visual_line(0)), '$ 0')
  1026. self.ae(lco(), '0x\n1x\n')
  1027. # first cmd output on screen
  1028. # test: find around
  1029. self.ae(fco(), '0\n1\n2')
  1030. s.scroll(2, False)
  1031. self.ae(s.scrolled_by, 2)
  1032. self.ae(str(s.visual_line(0)), '1')
  1033. self.ae(fco(), '0x\n1x\n')
  1034. # test: find downwards
  1035. s.scroll(2, False)
  1036. self.ae(str(s.visual_line(0)), '$ 1')
  1037. self.ae(fco(), '0x\n1x\n')
  1038. # resize
  1039. # get last cmd output with continued output mark
  1040. draw_prompt('3'), draw_output(1, 'long_line'), draw_output(2, 'l', False)
  1041. s.resize(4, 5)
  1042. s.scroll_to_prompt(-4)
  1043. self.ae(str(s.visual_line(0)), '$ 0')
  1044. self.ae(lco(), '0long_line\n0l\n1l\n')
  1045. # last visited cmd output
  1046. self.ae(lvco(), '0\n1\n2')
  1047. s.scroll_to_prompt(1)
  1048. self.ae(lvco(), '0x\n1x')
  1049. # last command output without line break
  1050. s = self.create_screen(cols=10, lines=3)
  1051. draw_prompt('p1')
  1052. mark_output(), s.draw('running')
  1053. self.ae(lco(), 'running')
  1054. s.index(), s.carriage_return()
  1055. self.ae(lco(), 'running\n')
  1056. # last command output from pager history
  1057. s = self.create_screen()
  1058. draw_prompt('p1')
  1059. draw_output(30)
  1060. self.ae(tuple(map(int, lco().split())), tuple(range(0, 30)))
  1061. # last non empty command output
  1062. s = self.create_screen()
  1063. draw_prompt('a'), draw_output(2, 'a')
  1064. draw_prompt('b'), mark_output()
  1065. self.ae(lco(), '')
  1066. self.ae(lco(which=3), '0a\n1a')
  1067. s.draw('running'), s.index(), s.carriage_return()
  1068. self.ae(lco(which=3), 'running\n')
  1069. s = self.create_screen()
  1070. draw_prompt('p1')
  1071. draw_output(30)
  1072. self.ae(tuple(map(int, lco(which=3).split())), tuple(range(0, 30)))
  1073. s = self.create_screen()
  1074. draw_prompt('p1')
  1075. draw_output(2, 'a')
  1076. draw_prompt('p1')
  1077. draw_prompt('p1')
  1078. self.ae(lco(which=3), '0a\n1a')
  1079. def test_pointer_shapes(self):
  1080. from kitty.window import set_pointer_shape
  1081. s = self.create_screen()
  1082. c = s.callbacks
  1083. response = ''
  1084. def cb(data):
  1085. nonlocal response
  1086. response = set_pointer_shape(s, str(data, 'utf-8'))
  1087. c.set_pointer_shape = cb
  1088. def send(a):
  1089. nonlocal response
  1090. response = ''
  1091. parse_bytes(s, f'\x1b]22;{a}\x1b\\'.encode())
  1092. return response
  1093. self.ae(send('?__current__'), '0')
  1094. self.ae(send('?__default__,__grabbed__,default,ne-resize,crosshair,XXX'), 'text,default,1,1,1,0')
  1095. def t(q, e=None):
  1096. self.ae(send(q), '')
  1097. self.ae(send('?__current__'), e)
  1098. t('default', 'default')
  1099. s.reset()
  1100. self.ae(send('?__current__'), '0')
  1101. t('=crosshair', 'crosshair')
  1102. t('<', '0')
  1103. t('=crosshair', 'crosshair')
  1104. t('', '0')
  1105. t('>help', 'help')
  1106. t('>wait', 'wait')
  1107. t('<', 'help')
  1108. t('<', '0')
  1109. t('default,help', 'help')
  1110. t('<', '0')
  1111. t('>default,help', 'help')
  1112. t('<', 'default')
  1113. t('<', '0')
  1114. t('=left_ptr', 'default')
  1115. t('=fleur', 'move')
  1116. def test_color_profile(self):
  1117. c = ColorProfile(defaults)
  1118. for i in range(8):
  1119. col = getattr(defaults, f'color{i}')
  1120. self.ae(c.as_color(i << 8 | 1), col)
  1121. self.ae(c.as_color(255 << 8 | 1), Color(0xee, 0xee, 0xee))
  1122. s = self.create_screen()
  1123. s.color_profile.reload_from_opts(defaults)
  1124. def q(send, expected=None):
  1125. s.callbacks.clear()
  1126. parse_bytes(s, b'\x1b]21;' + ';'.join(f'{k}={v}' for k, v in send.items()).encode() + b'\a')
  1127. self.ae(s.callbacks.color_control_responses, [expected] if expected else [])
  1128. q({k: '?' for k in 'background foreground 213 unknown'.split()}, {
  1129. 'background': defaults.background, 'foreground': defaults.foreground, '213': defaults.color213, 'unknown': '?'})
  1130. q({'background':'aquamarine'})
  1131. q({'background':'?', 'selection_background': '?'}, {'background': color_names['aquamarine'], 'selection_background': s.color_profile.highlight_bg})
  1132. q({'selection_background': ''})
  1133. self.assertIsNone(s.color_profile.highlight_bg)
  1134. q({'selection_background': '?'}, {'selection_background': ''})
  1135. s.color_profile.reload_from_opts(defaults)
  1136. q({'transparent_background_color9': '?'}, {'transparent_background_color9': '?'})
  1137. q({'transparent_background_color2': '?'}, {'transparent_background_color2': ''})
  1138. q({'transparent_background_color2': 'red@0.5'})
  1139. q({'transparent_background_color2': '?'}, {'transparent_background_color2': (Color(255, 0, 0), 126)})
  1140. q({'transparent_background_color2': '#ffffff@-1'})
  1141. q({'transparent_background_color2': '?'}, {'transparent_background_color2': (Color(255, 255, 255), 255)})