urwid 4.0.4__tar.gz → 4.0.5__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (316) hide show
  1. {urwid-4.0.4/urwid.egg-info → urwid-4.0.5}/PKG-INFO +1 -1
  2. {urwid-4.0.4 → urwid-4.0.5}/docs/changelog.rst +27 -0
  3. urwid-4.0.5/ruff-requirements.txt +1 -0
  4. {urwid-4.0.4 → urwid-4.0.5}/tests/test_scrollable.py +66 -0
  5. {urwid-4.0.4 → urwid-4.0.5}/tests/test_signals.py +24 -0
  6. {urwid-4.0.4 → urwid-4.0.5}/tests/test_widget.py +52 -0
  7. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/_posix_raw_display.py +1 -1
  8. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/_raw_display_base.py +2 -4
  9. {urwid-4.0.4 → urwid-4.0.5}/urwid/signals.py +1 -1
  10. {urwid-4.0.4 → urwid-4.0.5}/urwid/split_repr.py +1 -1
  11. {urwid-4.0.4 → urwid-4.0.5}/urwid/util.py +12 -7
  12. {urwid-4.0.4 → urwid-4.0.5}/urwid/version.py +3 -3
  13. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/attr_map.py +6 -2
  14. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/edit.py +110 -48
  15. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/filler.py +10 -5
  16. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/pile.py +4 -3
  17. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/scrollable.py +82 -15
  18. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/widget.py +1 -1
  19. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/widget_decoration.py +11 -6
  20. {urwid-4.0.4 → urwid-4.0.5/urwid.egg-info}/PKG-INFO +1 -1
  21. {urwid-4.0.4 → urwid-4.0.5}/urwid.egg-info/scm_file_list.json +265 -265
  22. urwid-4.0.5/urwid.egg-info/scm_version.json +8 -0
  23. urwid-4.0.4/ruff-requirements.txt +0 -1
  24. urwid-4.0.4/urwid.egg-info/scm_version.json +0 -8
  25. {urwid-4.0.4 → urwid-4.0.5}/.coveralls.yml +0 -0
  26. {urwid-4.0.4 → urwid-4.0.5}/.devcontainer/dev.dockerfile +0 -0
  27. {urwid-4.0.4 → urwid-4.0.5}/.devcontainer/devcontainer.json +0 -0
  28. {urwid-4.0.4 → urwid-4.0.5}/.dockerignore +0 -0
  29. {urwid-4.0.4 → urwid-4.0.5}/.editorconfig +0 -0
  30. {urwid-4.0.4 → urwid-4.0.5}/.github/CODE_OF_CONDUCT.md +0 -0
  31. {urwid-4.0.4 → urwid-4.0.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  32. {urwid-4.0.4 → urwid-4.0.5}/.github/ISSUE_TEMPLATE/docs.md +0 -0
  33. {urwid-4.0.4 → urwid-4.0.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  34. {urwid-4.0.4 → urwid-4.0.5}/.github/ISSUE_TEMPLATE/question.md +0 -0
  35. {urwid-4.0.4 → urwid-4.0.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  36. {urwid-4.0.4 → urwid-4.0.5}/.github/dependabot.yml +0 -0
  37. {urwid-4.0.4 → urwid-4.0.5}/.github/labeler.yml +0 -0
  38. {urwid-4.0.4 → urwid-4.0.5}/.github/release.yml +0 -0
  39. {urwid-4.0.4 → urwid-4.0.5}/.github/workflows/documentation.yml +0 -0
  40. {urwid-4.0.4 → urwid-4.0.5}/.github/workflows/isolated_static_check.yml +0 -0
  41. {urwid-4.0.4 → urwid-4.0.5}/.github/workflows/labels.yml +0 -0
  42. {urwid-4.0.4 → urwid-4.0.5}/.github/workflows/pythonpackage.yml +0 -0
  43. {urwid-4.0.4 → urwid-4.0.5}/.github/workflows/yamllint.yml +0 -0
  44. {urwid-4.0.4 → urwid-4.0.5}/.gitignore +0 -0
  45. {urwid-4.0.4 → urwid-4.0.5}/.pre-commit-config.yaml +0 -0
  46. {urwid-4.0.4 → urwid-4.0.5}/.yamllint.yml +0 -0
  47. {urwid-4.0.4 → urwid-4.0.5}/COPYING +0 -0
  48. {urwid-4.0.4 → urwid-4.0.5}/MANIFEST.in +0 -0
  49. {urwid-4.0.4 → urwid-4.0.5}/README.rst +0 -0
  50. {urwid-4.0.4 → urwid-4.0.5}/SECURITY.md +0 -0
  51. {urwid-4.0.4 → urwid-4.0.5}/classifiers.txt +0 -0
  52. {urwid-4.0.4 → urwid-4.0.5}/docs/Makefile +0 -0
  53. {urwid-4.0.4 → urwid-4.0.5}/docs/conf.py +0 -0
  54. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/bigtext.py +0 -0
  55. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/bigtext.py.xdotool +0 -0
  56. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/bigtext1.png +0 -0
  57. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/bigtext2.png +0 -0
  58. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/bigtext3.png +0 -0
  59. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/browse.py +0 -0
  60. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/browse.py.xdotool +0 -0
  61. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/browse1.png +0 -0
  62. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/browse2.png +0 -0
  63. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/edit.py +0 -0
  64. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/edit.py.xdotool +0 -0
  65. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/edit1.png +0 -0
  66. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/edit2.png +0 -0
  67. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/edit_text.txt +0 -0
  68. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/graph.py +0 -0
  69. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/graph.py.xdotool +0 -0
  70. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/graph1.png +0 -0
  71. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/graph2.png +0 -0
  72. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/index.rst +0 -0
  73. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/palette_test.py +0 -0
  74. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/palette_test.py.xdotool +0 -0
  75. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/palette_test1.png +0 -0
  76. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/palette_test2.png +0 -0
  77. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/pop_up.py +0 -0
  78. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/pop_up.py.xdotool +0 -0
  79. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/pop_up1.png +0 -0
  80. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/pop_up2.png +0 -0
  81. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/real_browse.py +0 -0
  82. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/real_edit.py +0 -0
  83. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/subproc.py +0 -0
  84. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/subproc.py.xdotool +0 -0
  85. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/subproc1.png +0 -0
  86. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/subproc2.png +0 -0
  87. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/subproc2.py +0 -0
  88. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/tour.py +0 -0
  89. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/tour.py.xdotool +0 -0
  90. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/tour1.png +0 -0
  91. {urwid-4.0.4 → urwid-4.0.5}/docs/examples/tour2.png +0 -0
  92. {urwid-4.0.4 → urwid-4.0.5}/docs/index.rst +0 -0
  93. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/bright_combinations.py +0 -0
  94. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/bright_combinations.py.xdotool +0 -0
  95. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/bright_combinations1.png +0 -0
  96. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/canvascache.rst +0 -0
  97. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/displayattributes.rst +0 -0
  98. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/displaymodules.rst +0 -0
  99. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/encodings.rst +0 -0
  100. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/images/display_modules.png +0 -0
  101. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/images/introduction.png +0 -0
  102. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/images/urwid_widgets.svgz +0 -0
  103. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/images/urwid_widgets_1.png +0 -0
  104. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/images/urwid_widgets_1.xcf +0 -0
  105. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/images/urwid_widgets_2.png +0 -0
  106. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/images/urwid_widgets_2.xcf +0 -0
  107. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/images/widget_layout.png +0 -0
  108. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/index.rst +0 -0
  109. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/mainloop.rst +0 -0
  110. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/overview.rst +0 -0
  111. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/safe_combinations.py +0 -0
  112. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/safe_combinations.py.xdotool +0 -0
  113. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/safe_combinations1.png +0 -0
  114. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/textlayout.rst +0 -0
  115. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/userinput.rst +0 -0
  116. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/wanat.py +0 -0
  117. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/wanat_multi.py +0 -0
  118. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/wcur1.py +0 -0
  119. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/wcur2.py +0 -0
  120. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/widgets.rst +0 -0
  121. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/wmod.py +0 -0
  122. {urwid-4.0.4 → urwid-4.0.5}/docs/manual/wsel.py +0 -0
  123. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/attrspec.rst +0 -0
  124. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/canvas.rst +0 -0
  125. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/command_map.rst +0 -0
  126. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/constants.rst +0 -0
  127. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/deprecated.rst +0 -0
  128. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/display_modules.rst +0 -0
  129. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/exceptions.rst +0 -0
  130. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/global_settings.rst +0 -0
  131. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/index.rst +0 -0
  132. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/list_walkers.rst +0 -0
  133. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/main_loop.rst +0 -0
  134. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/meta.rst +0 -0
  135. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/signals.rst +0 -0
  136. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/text_layout.rst +0 -0
  137. {urwid-4.0.4 → urwid-4.0.5}/docs/reference/widget.rst +0 -0
  138. {urwid-4.0.4 → urwid-4.0.5}/docs/tools/compile_pngs.sh +0 -0
  139. {urwid-4.0.4 → urwid-4.0.5}/docs/tools/screenshots.sh +0 -0
  140. {urwid-4.0.4 → urwid-4.0.5}/docs/tools/static/.placeholder +0 -0
  141. {urwid-4.0.4 → urwid-4.0.5}/docs/tools/templates/indexcontent.html +0 -0
  142. {urwid-4.0.4 → urwid-4.0.5}/docs/tools/templates/indexsidebar.html +0 -0
  143. {urwid-4.0.4 → urwid-4.0.5}/docs/tools/templates/localtoc.html +0 -0
  144. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/adventure.py +0 -0
  145. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/adventure.py.xdotool +0 -0
  146. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/adventure1.png +0 -0
  147. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/adventure2.png +0 -0
  148. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/adventure3.png +0 -0
  149. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/adventure4.png +0 -0
  150. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/cmenu.py +0 -0
  151. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/cmenu.py.xdotool +0 -0
  152. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/cmenu1.png +0 -0
  153. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/cmenu2.png +0 -0
  154. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/cmenu3.png +0 -0
  155. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/cmenu4.png +0 -0
  156. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/highcolors.py +0 -0
  157. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/highcolors.py.xdotool +0 -0
  158. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/highcolors1.png +0 -0
  159. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/hmenu.py +0 -0
  160. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/hmenu.py.xdotool +0 -0
  161. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/hmenu1.png +0 -0
  162. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/hmenu2.png +0 -0
  163. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/hmenu3.png +0 -0
  164. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/hmenu4.png +0 -0
  165. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/index.rst +0 -0
  166. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/input.py +0 -0
  167. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/input.py.xdotool +0 -0
  168. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/input1.png +0 -0
  169. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/input2.png +0 -0
  170. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/input3.png +0 -0
  171. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/input4.png +0 -0
  172. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/input5.png +0 -0
  173. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/menu25.png +0 -0
  174. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/minimal.py +0 -0
  175. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/minimal.py.xdotool +0 -0
  176. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/minimal1.png +0 -0
  177. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/multiple.py +0 -0
  178. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/multiple.py.xdotool +0 -0
  179. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/multiple1.png +0 -0
  180. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/multiple2.png +0 -0
  181. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/multiple3.png +0 -0
  182. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/multiple4.png +0 -0
  183. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/qa.py +0 -0
  184. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/qa.py.xdotool +0 -0
  185. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/qa1.png +0 -0
  186. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/qa2.png +0 -0
  187. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/qa3.png +0 -0
  188. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/sig.py +0 -0
  189. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/sig.py.xdotool +0 -0
  190. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/sig1.png +0 -0
  191. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/sig2.png +0 -0
  192. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/sig3.png +0 -0
  193. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/sig4.png +0 -0
  194. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/smenu.py +0 -0
  195. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/smenu.py.xdotool +0 -0
  196. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/smenu1.png +0 -0
  197. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/smenu2.png +0 -0
  198. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/smenu3.png +0 -0
  199. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/urwid_attr.py +0 -0
  200. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/urwid_attr.py.xdotool +0 -0
  201. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/urwid_attr1.png +0 -0
  202. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/urwid_attr2.png +0 -0
  203. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/urwid_attr3.png +0 -0
  204. {urwid-4.0.4 → urwid-4.0.5}/docs/tutorial/urwid_attr4.png +0 -0
  205. {urwid-4.0.4 → urwid-4.0.5}/docs/urwid-logo.png +0 -0
  206. {urwid-4.0.4 → urwid-4.0.5}/examples/asyncio_socket_server.py +0 -0
  207. {urwid-4.0.4 → urwid-4.0.5}/examples/bigtext.py +0 -0
  208. {urwid-4.0.4 → urwid-4.0.5}/examples/browse.py +0 -0
  209. {urwid-4.0.4 → urwid-4.0.5}/examples/calc.py +0 -0
  210. {urwid-4.0.4 → urwid-4.0.5}/examples/dialog.py +0 -0
  211. {urwid-4.0.4 → urwid-4.0.5}/examples/edit.py +0 -0
  212. {urwid-4.0.4 → urwid-4.0.5}/examples/fib.py +0 -0
  213. {urwid-4.0.4 → urwid-4.0.5}/examples/graph.py +0 -0
  214. {urwid-4.0.4 → urwid-4.0.5}/examples/input_test.py +0 -0
  215. {urwid-4.0.4 → urwid-4.0.5}/examples/lcd_cf635.py +0 -0
  216. {urwid-4.0.4 → urwid-4.0.5}/examples/palette_test.py +0 -0
  217. {urwid-4.0.4 → urwid-4.0.5}/examples/pop_up.py +0 -0
  218. {urwid-4.0.4 → urwid-4.0.5}/examples/subproc.py +0 -0
  219. {urwid-4.0.4 → urwid-4.0.5}/examples/subproc2.py +0 -0
  220. {urwid-4.0.4 → urwid-4.0.5}/examples/terminal.py +0 -0
  221. {urwid-4.0.4 → urwid-4.0.5}/examples/tour.py +0 -0
  222. {urwid-4.0.4 → urwid-4.0.5}/examples/treesample.py +0 -0
  223. {urwid-4.0.4 → urwid-4.0.5}/examples/twisted_serve_ssh.py +0 -0
  224. {urwid-4.0.4 → urwid-4.0.5}/examples/twisted_serve_ssh.tac +0 -0
  225. {urwid-4.0.4 → urwid-4.0.5}/pylint-requirements.txt +0 -0
  226. {urwid-4.0.4 → urwid-4.0.5}/pyproject.toml +0 -0
  227. {urwid-4.0.4 → urwid-4.0.5}/requirements.txt +0 -0
  228. {urwid-4.0.4 → urwid-4.0.5}/setup.cfg +0 -0
  229. {urwid-4.0.4 → urwid-4.0.5}/test_requirements.txt +0 -0
  230. {urwid-4.0.4 → urwid-4.0.5}/tests/__init__.py +0 -0
  231. {urwid-4.0.4 → urwid-4.0.5}/tests/test_canvas.py +0 -0
  232. {urwid-4.0.4 → urwid-4.0.5}/tests/test_columns.py +0 -0
  233. {urwid-4.0.4 → urwid-4.0.5}/tests/test_container.py +0 -0
  234. {urwid-4.0.4 → urwid-4.0.5}/tests/test_doctests.py +0 -0
  235. {urwid-4.0.4 → urwid-4.0.5}/tests/test_escapes.py +0 -0
  236. {urwid-4.0.4 → urwid-4.0.5}/tests/test_event_loops.py +0 -0
  237. {urwid-4.0.4 → urwid-4.0.5}/tests/test_filler.py +0 -0
  238. {urwid-4.0.4 → urwid-4.0.5}/tests/test_floatedit.py +0 -0
  239. {urwid-4.0.4 → urwid-4.0.5}/tests/test_font.py +0 -0
  240. {urwid-4.0.4 → urwid-4.0.5}/tests/test_frame.py +0 -0
  241. {urwid-4.0.4 → urwid-4.0.5}/tests/test_grapheme_clusters.py +0 -0
  242. {urwid-4.0.4 → urwid-4.0.5}/tests/test_graphics.py +0 -0
  243. {urwid-4.0.4 → urwid-4.0.5}/tests/test_grid_flow.py +0 -0
  244. {urwid-4.0.4 → urwid-4.0.5}/tests/test_line_box.py +0 -0
  245. {urwid-4.0.4 → urwid-4.0.5}/tests/test_listbox.py +0 -0
  246. {urwid-4.0.4 → urwid-4.0.5}/tests/test_main_loop.py +0 -0
  247. {urwid-4.0.4 → urwid-4.0.5}/tests/test_overlay.py +0 -0
  248. {urwid-4.0.4 → urwid-4.0.5}/tests/test_padding.py +0 -0
  249. {urwid-4.0.4 → urwid-4.0.5}/tests/test_pile.py +0 -0
  250. {urwid-4.0.4 → urwid-4.0.5}/tests/test_raw_display.py +0 -0
  251. {urwid-4.0.4 → urwid-4.0.5}/tests/test_str_util.py +0 -0
  252. {urwid-4.0.4 → urwid-4.0.5}/tests/test_text_layout.py +0 -0
  253. {urwid-4.0.4 → urwid-4.0.5}/tests/test_tree.py +0 -0
  254. {urwid-4.0.4 → urwid-4.0.5}/tests/test_util.py +0 -0
  255. {urwid-4.0.4 → urwid-4.0.5}/tests/test_vterm.py +0 -0
  256. {urwid-4.0.4 → urwid-4.0.5}/tests/util.py +0 -0
  257. {urwid-4.0.4 → urwid-4.0.5}/tox.ini +0 -0
  258. {urwid-4.0.4 → urwid-4.0.5}/urwid/__init__.py +0 -0
  259. {urwid-4.0.4 → urwid-4.0.5}/urwid/canvas.py +0 -0
  260. {urwid-4.0.4 → urwid-4.0.5}/urwid/command_map.py +0 -0
  261. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/__init__.py +0 -0
  262. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/_web.css +0 -0
  263. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/_web.js +0 -0
  264. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/_win32.py +0 -0
  265. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/_win32_raw_display.py +0 -0
  266. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/common.py +0 -0
  267. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/curses.py +0 -0
  268. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/escape.py +0 -0
  269. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/html_fragment.py +0 -0
  270. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/lcd.py +0 -0
  271. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/raw.py +0 -0
  272. {urwid-4.0.4 → urwid-4.0.5}/urwid/display/web.py +0 -0
  273. {urwid-4.0.4 → urwid-4.0.5}/urwid/event_loop/__init__.py +0 -0
  274. {urwid-4.0.4 → urwid-4.0.5}/urwid/event_loop/abstract_loop.py +0 -0
  275. {urwid-4.0.4 → urwid-4.0.5}/urwid/event_loop/asyncio_loop.py +0 -0
  276. {urwid-4.0.4 → urwid-4.0.5}/urwid/event_loop/glib_loop.py +0 -0
  277. {urwid-4.0.4 → urwid-4.0.5}/urwid/event_loop/main_loop.py +0 -0
  278. {urwid-4.0.4 → urwid-4.0.5}/urwid/event_loop/select_loop.py +0 -0
  279. {urwid-4.0.4 → urwid-4.0.5}/urwid/event_loop/tornado_loop.py +0 -0
  280. {urwid-4.0.4 → urwid-4.0.5}/urwid/event_loop/trio_loop.py +0 -0
  281. {urwid-4.0.4 → urwid-4.0.5}/urwid/event_loop/twisted_loop.py +0 -0
  282. {urwid-4.0.4 → urwid-4.0.5}/urwid/event_loop/zmq_loop.py +0 -0
  283. {urwid-4.0.4 → urwid-4.0.5}/urwid/font.py +0 -0
  284. {urwid-4.0.4 → urwid-4.0.5}/urwid/graphics.py +0 -0
  285. {urwid-4.0.4 → urwid-4.0.5}/urwid/numedit.py +0 -0
  286. {urwid-4.0.4 → urwid-4.0.5}/urwid/py.typed +0 -0
  287. {urwid-4.0.4 → urwid-4.0.5}/urwid/str_util.py +0 -0
  288. {urwid-4.0.4 → urwid-4.0.5}/urwid/text_layout.py +0 -0
  289. {urwid-4.0.4 → urwid-4.0.5}/urwid/vterm.py +0 -0
  290. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/__init__.py +0 -0
  291. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/attr_wrap.py +0 -0
  292. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/bar_graph.py +0 -0
  293. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/big_text.py +0 -0
  294. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/box_adapter.py +0 -0
  295. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/columns.py +0 -0
  296. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/constants.py +0 -0
  297. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/container.py +0 -0
  298. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/divider.py +0 -0
  299. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/frame.py +0 -0
  300. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/grid_flow.py +0 -0
  301. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/line_box.py +0 -0
  302. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/listbox.py +0 -0
  303. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/monitored_list.py +0 -0
  304. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/overlay.py +0 -0
  305. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/padding.py +0 -0
  306. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/popup.py +0 -0
  307. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/progress_bar.py +0 -0
  308. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/solid_fill.py +0 -0
  309. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/text.py +0 -0
  310. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/treetools.py +0 -0
  311. {urwid-4.0.4 → urwid-4.0.5}/urwid/widget/wimp.py +0 -0
  312. {urwid-4.0.4 → urwid-4.0.5}/urwid.egg-info/SOURCES.txt +0 -0
  313. {urwid-4.0.4 → urwid-4.0.5}/urwid.egg-info/dependency_links.txt +0 -0
  314. {urwid-4.0.4 → urwid-4.0.5}/urwid.egg-info/not-zip-safe +0 -0
  315. {urwid-4.0.4 → urwid-4.0.5}/urwid.egg-info/requires.txt +0 -0
  316. {urwid-4.0.4 → urwid-4.0.5}/urwid.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: urwid
3
- Version: 4.0.4
3
+ Version: 4.0.5
4
4
  Summary: A full-featured console (xterm et al.) user interface library
5
5
  Author-email: Ian Ward <ian@excess.org>
6
6
  License-Expression: LGPL-2.1-only
@@ -2,6 +2,33 @@
2
2
  Changelog
3
3
  ---------
4
4
 
5
+ Urwid 4.0.5
6
+ ===========
7
+
8
+ 2026-07-20
9
+
10
+ New features 🗹
11
+ +++++++++++++++
12
+ * Copy signal handlers before emission by @Sanjays2402 in https://github.com/urwid/urwid/pull/1187
13
+ * Implement `_scrollbar_layout` and left click handling for `Scrollable` by @penguinolog in https://github.com/urwid/urwid/pull/1192
14
+ Bug fixes 🕷
15
+ ++++++++++++
16
+ * Fix rle_product for zero-len elements by @penguinolog in https://github.com/urwid/urwid/pull/1191
17
+ Documentation 🕮
18
+ ++++++++++++++++
19
+ * typing: fix `Filler` typing annotations by @penguinolog in https://github.com/urwid/urwid/pull/1183
20
+ * typing: extend widget_decoration module typing coverage by @penguinolog in https://github.com/urwid/urwid/pull/1184
21
+ * Typing: annotate and fix attr_map by @penguinolog in https://github.com/urwid/urwid/pull/1186
22
+ Refactoring 🛠
23
+ ++++++++++++++
24
+ * Migrate `Edit` widget to use `WidgetWrap` as base class and update methods by @penguinolog in https://github.com/urwid/urwid/pull/1180
25
+
26
+ New Contributors
27
+ ++++++++++++++++
28
+ * @Sanjays2402 made their first contribution in https://github.com/urwid/urwid/pull/1187
29
+
30
+ **Full Changelog**: https://github.com/urwid/urwid/compare/4.0.4...4.0.5
31
+
5
32
  Urwid 4.0.4
6
33
  ===========
7
34
 
@@ -0,0 +1 @@
1
+ ruff==0.15.22
@@ -142,6 +142,72 @@ class TestScrollBarScrollable(unittest.TestCase):
142
142
 
143
143
  self.assertEqual(top_position_rendered, widget.render(reduced_size).decoded_text)
144
144
 
145
+ def test_mouse_left_click_scrollbar(self):
146
+ """Left click on the rendered scrollbar jumps to the clicked position."""
147
+ content = urwid.Text("\n".join(string.ascii_letters)) # 52 single-char lines
148
+ reduced_size = (3, 5)
149
+ widget = urwid.ScrollBar(urwid.Scrollable(content))
150
+ scrollable = widget.original_widget
151
+ scrollbar_col = reduced_size[0] - 1 # right side scrollbar
152
+
153
+ self.assertEqual(0, scrollable.get_scrollpos())
154
+
155
+ # posmax == 52 - 5 == 47, thumb_height == 1, thumb travel == 5 - 1 == 4.
156
+ # Clicking the bottom row moves past the end and is clamped to posmax.
157
+ self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 4, False))
158
+ self.assertEqual(47, scrollable.get_scrollpos())
159
+
160
+ # Clicking the top row scrolls back to the top.
161
+ self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 0, False))
162
+ self.assertEqual(0, scrollable.get_scrollpos())
163
+
164
+ # Intermediate rows map proportionally: round(row * posmax / travel).
165
+ self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 1, False))
166
+ self.assertEqual(12, scrollable.get_scrollpos())
167
+
168
+ self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, scrollbar_col, 3, False))
169
+ self.assertEqual(35, scrollable.get_scrollpos())
170
+
171
+ def test_mouse_left_click_content_ignored(self):
172
+ """Left click outside of the scrollbar columns does not scroll."""
173
+ content = urwid.Text("\n".join(string.ascii_letters))
174
+ reduced_size = (3, 5)
175
+ widget = urwid.ScrollBar(urwid.Scrollable(content))
176
+ scrollable = widget.original_widget
177
+
178
+ self.assertEqual(0, scrollable.get_scrollpos())
179
+
180
+ # Column 0 belongs to the wrapped content, not the scrollbar.
181
+ self.assertFalse(widget.mouse_event(reduced_size, "mouse press", 1, 0, 4, False))
182
+ self.assertEqual(0, scrollable.get_scrollpos())
183
+
184
+ def test_mouse_left_click_left_side_scrollbar(self):
185
+ """Left click detection honours a left-aligned scrollbar."""
186
+ content = urwid.Text("\n".join(string.ascii_letters))
187
+ reduced_size = (3, 5)
188
+ widget = urwid.ScrollBar(urwid.Scrollable(content), side="left")
189
+ scrollable = widget.original_widget
190
+
191
+ self.assertEqual(0, scrollable.get_scrollpos())
192
+
193
+ # The scrollbar occupies column 0 when aligned to the left.
194
+ self.assertTrue(widget.mouse_event(reduced_size, "mouse press", 1, 0, 4, False))
195
+ self.assertEqual(47, scrollable.get_scrollpos())
196
+
197
+ # A click on the content columns is ignored.
198
+ self.assertFalse(widget.mouse_event(reduced_size, "mouse press", 1, 1, 0, False))
199
+ self.assertEqual(47, scrollable.get_scrollpos())
200
+
201
+ def test_mouse_left_click_without_scrollbar(self):
202
+ """Without a rendered scrollbar a left click is not handled."""
203
+ content = urwid.Text("a\nb") # fits into the available height
204
+ reduced_size = (3, 5)
205
+ widget = urwid.ScrollBar(urwid.Scrollable(content))
206
+ scrollable = widget.original_widget
207
+
208
+ self.assertFalse(widget.mouse_event(reduced_size, "mouse press", 1, 2, 2, False))
209
+ self.assertEqual(0, scrollable.get_scrollpos())
210
+
145
211
  def test_alt_symbols(self):
146
212
  long_content = urwid.Text(LGPL_HEADER)
147
213
  reduced_size = (40, 5)
@@ -25,6 +25,30 @@ class SiglnalsTest(unittest.TestCase):
25
25
  edit.set_edit_text("another text")
26
26
  handler.assert_not_called()
27
27
 
28
+ def test_handler_mutation_during_emit(self):
29
+ emitter = self.EmClass()
30
+ calls = []
31
+
32
+ def third():
33
+ calls.append("third")
34
+
35
+ def first():
36
+ calls.append("first")
37
+ disconnect_signal(emitter, "test", first)
38
+ connect_signal(emitter, "test", third)
39
+
40
+ def second():
41
+ calls.append("second")
42
+
43
+ connect_signal(emitter, "test", first)
44
+ connect_signal(emitter, "test", second)
45
+
46
+ emit_signal(emitter, "test")
47
+ self.assertEqual(calls, ["first", "second"])
48
+
49
+ emit_signal(emitter, "test")
50
+ self.assertEqual(calls, ["first", "second", "second", "third"])
51
+
28
52
  @unittest.skipIf(sys.implementation.name == "pypy", "WeakRef works differently on PyPy")
29
53
  def test_weak_del(self):
30
54
  emitter = SiglnalsTest.EmClass()
@@ -39,6 +39,58 @@ class TextTest(unittest.TestCase):
39
39
  got = urwid.Text("û").render((3,))._text
40
40
  assert got == expected, f"got: {got!r} expected: {expected!r}"
41
41
 
42
+ def test_empty_markup_segment_middle(self) -> None:
43
+ """An empty markup segment in the middle must not truncate the rendered row."""
44
+ widget = urwid.Text([("a", "X" * 10), ("b", ""), ("c", " END")])
45
+
46
+ self.assertEqual(("XXXXXXXXXX END", [("a", 10), ("b", 0), ("c", 4)]), widget.get_text())
47
+
48
+ canvas = widget.render(())
49
+ self.assertEqual([b"XXXXXXXXXX END"], canvas.text)
50
+ self.assertEqual(("XXXXXXXXXX END",), canvas.decoded_text)
51
+
52
+ def test_empty_markup_segment_start(self) -> None:
53
+ """An empty markup segment at the start must keep the whole rendered row."""
54
+ widget = urwid.Text([("b", ""), ("a", "X" * 10), ("c", " END")])
55
+
56
+ self.assertEqual(("XXXXXXXXXX END", [("b", 0), ("a", 10), ("c", 4)]), widget.get_text())
57
+
58
+ canvas = widget.render(())
59
+ self.assertEqual([b"XXXXXXXXXX END"], canvas.text)
60
+ self.assertEqual(("XXXXXXXXXX END",), canvas.decoded_text)
61
+
62
+ def test_empty_markup_segment_end(self) -> None:
63
+ """An empty markup segment at the end must keep the whole rendered row."""
64
+ widget = urwid.Text([("a", "X" * 10), ("c", " END"), ("b", "")])
65
+
66
+ self.assertEqual(("XXXXXXXXXX END", [("a", 10), ("c", 4), ("b", 0)]), widget.get_text())
67
+
68
+ canvas = widget.render(())
69
+ self.assertEqual([b"XXXXXXXXXX END"], canvas.text)
70
+ self.assertEqual(("XXXXXXXXXX END",), canvas.decoded_text)
71
+
72
+ def test_multiple_empty_markup_segments(self) -> None:
73
+ """Several consecutive empty markup segments must not drop following content."""
74
+ widget = urwid.Text([("a", "AB"), ("b", ""), ("c", ""), ("d", "CD")])
75
+
76
+ self.assertEqual(("ABCD", [("a", 2), ("b", 0), ("c", 0), ("d", 2)]), widget.get_text())
77
+
78
+ canvas = widget.render(())
79
+ self.assertEqual([b"ABCD"], canvas.text)
80
+ self.assertEqual(("ABCD",), canvas.decoded_text)
81
+
82
+ def test_empty_markup_segment_render_content(self) -> None:
83
+ """The canvas content rows must expose the display attribute of each segment."""
84
+ widget = urwid.Text([("a", "X" * 10), ("b", ""), ("c", " END")])
85
+
86
+ content = list(widget.render(()).content())
87
+ attrs = [attr for row in content for attr, _cs, _text in row]
88
+ rendered = b"".join(text for row in content for _attr, _cs, text in row)
89
+
90
+ self.assertEqual(b"XXXXXXXXXX END", rendered)
91
+ self.assertIn("a", attrs)
92
+ self.assertIn("c", attrs)
93
+
42
94
 
43
95
  class EditTest(unittest.TestCase):
44
96
  def setUp(self):
@@ -333,7 +333,7 @@ class Screen(_raw_display_base.Screen):
333
333
  input_ready = selector.select(0)
334
334
  while input_ready:
335
335
  chunk = os.read(fd, 1024)
336
- if len(chunk) == 0:
336
+ if not chunk:
337
337
  raise RuntimeError("stdin has been closed")
338
338
  chars.extend(chunk)
339
339
  input_ready = selector.select(0)
@@ -483,7 +483,7 @@ class Screen(BaseScreen, RealTerminal):
483
483
  try:
484
484
  while codes:
485
485
  run, remaining_codes = escape.process_keyqueue(codes, wait_for_more)
486
- codes = list(remaining_codes)
486
+ codes = remaining_codes.copy()
487
487
  decoded_codes.extend(run)
488
488
  except escape.MoreInputRequired:
489
489
  # Set a timer to wait for the rest of the input; if it goes off
@@ -587,9 +587,7 @@ class Screen(BaseScreen, RealTerminal):
587
587
  run: bytes,
588
588
  last: bool,
589
589
  ) -> None:
590
- nonlocal last_charset_flag # type: ignore[misc]
591
- nonlocal last_attributes # type: ignore[misc]
592
- nonlocal first # type: ignore[misc]
590
+ nonlocal last_charset_flag, last_attributes, first # type: ignore[misc]
593
591
 
594
592
  if not isinstance(run, bytes): # canvases render with bytes
595
593
  raise TypeError(run)
@@ -293,7 +293,7 @@ class Signals:
293
293
  """
294
294
  result = False
295
295
  handlers = getattr(obj, self._signal_attr, {}).get(name, [])
296
- for _key, callback, user_arg, (weak_args, user_args) in handlers:
296
+ for _key, callback, user_arg, (weak_args, user_args) in handlers.copy():
297
297
  result |= self._call_callback(callback, user_arg, weak_args, user_args, args)
298
298
  return result
299
299
 
@@ -52,7 +52,7 @@ def split_repr(self: Widget) -> str:
52
52
  >>> Bar()
53
53
  <Bar words here too attrs='appear too' barttr=42>
54
54
  """
55
- alist = sorted((str(k), normalize_repr(v)) for k, v in self._repr_attrs().items())
55
+ alist = sorted((k, normalize_repr(v)) for k, v in self._repr_attrs().items())
56
56
 
57
57
  words = self._repr_words()
58
58
  if not words and not alist:
@@ -429,17 +429,22 @@ def rle_product(
429
429
  a2, r2 = rle2[0]
430
430
 
431
431
  result: list[tuple[tuple[Hashable, Hashable], int]] = []
432
- while r1 and r2:
433
- r = min(r1, r2)
434
- rle_append_modify(result, ((a1, a2), r)) # type: ignore[arg-type]
435
- r1 -= r
436
- if r1 == 0 and i1 < len(rle1):
432
+ while True:
433
+ # Skip zero-length runs (e.g. produced by empty markup segments): they carry no
434
+ # columns but must not stop the merge early, otherwise following runs are dropped.
435
+ while r1 == 0 and i1 < len(rle1):
437
436
  a1, r1 = rle1[i1]
438
437
  i1 += 1
439
- r2 -= r
440
- if r2 == 0 and i2 < len(rle2):
438
+ while r2 == 0 and i2 < len(rle2):
441
439
  a2, r2 = rle2[i2]
442
440
  i2 += 1
441
+ if r1 == 0 or r2 == 0:
442
+ break
443
+
444
+ r = min(r1, r2)
445
+ rle_append_modify(result, ((a1, a2), r)) # type: ignore[arg-type]
446
+ r1 -= r
447
+ r2 -= r
443
448
  return result
444
449
 
445
450
 
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '4.0.4'
22
- __version_tuple__ = version_tuple = (4, 0, 4)
21
+ __version__ = version = '4.0.5'
22
+ __version_tuple__ = version_tuple = (4, 0, 5)
23
23
 
24
- __commit_id__ = commit_id = 'g171c58bd2'
24
+ __commit_id__ = commit_id = 'gecd583268'
@@ -90,7 +90,7 @@ class AttrMap(delegate_to_widget_mixin("_original_widget"), WidgetDecoration[Wra
90
90
  # FIXME: a dictionary that detects modifications would be better
91
91
  return dict(self._attr_map)
92
92
 
93
- def set_attr_map(self, attr_map: dict[Hashable, Hashable] | None) -> None:
93
+ def set_attr_map(self, attr_map: dict[Hashable, Hashable]) -> None:
94
94
  """
95
95
  Set the attribute mapping dictionary {from_attr: to_attr, ...}
96
96
 
@@ -152,7 +152,11 @@ class AttrMap(delegate_to_widget_mixin("_original_widget"), WidgetDecoration[Wra
152
152
 
153
153
  focus_map = property(get_focus_map, set_focus_map)
154
154
 
155
- def render(self, size, focus: bool = False) -> CompositeCanvas:
155
+ def render(
156
+ self,
157
+ size: tuple[()] | tuple[int] | tuple[int, int],
158
+ focus: bool = False,
159
+ ) -> CompositeCanvas:
156
160
  """
157
161
  Render wrapped widget and apply attribute. Return canvas.
158
162
  """
@@ -4,7 +4,7 @@ import string
4
4
  import typing
5
5
 
6
6
  from urwid import text_layout
7
- from urwid.canvas import CompositeCanvas
7
+ from urwid.canvas import CompositeCanvas, apply_text_layout
8
8
  from urwid.command_map import Command
9
9
  from urwid.split_repr import remove_defaults
10
10
  from urwid.str_util import is_wide_char, move_next_char, move_prev_char
@@ -12,6 +12,7 @@ from urwid.util import decompose_tagmarkup
12
12
 
13
13
  from .constants import Align, Sizing, WrapMode
14
14
  from .text import Text, TextError
15
+ from .widget import WidgetWrap
15
16
 
16
17
  if typing.TYPE_CHECKING:
17
18
  from collections.abc import Hashable
@@ -27,7 +28,7 @@ class EditError(TextError):
27
28
  pass
28
29
 
29
30
 
30
- class Edit(Text):
31
+ class Edit(WidgetWrap[Text]):
31
32
  """
32
33
  Text editing widget implements cursor movement, text insertion and
33
34
  deletion. A caption may prefix the editing area. Uses text class
@@ -78,12 +79,10 @@ class Edit(Text):
78
79
  mask: str | None = None,
79
80
  ) -> None:
80
81
  """
81
- :param caption: markup for caption preceding edit_text, see
82
- :class:`Text` for description of text markup.
82
+ :param caption: markup for caption preceding edit_text, see :class:`Text` for description of text markup.
83
83
  :type caption: text markup
84
- :param edit_text: initial text for editing, type (bytes or unicode)
85
- must match the text in the caption
86
- :type edit_text: bytes or unicode
84
+ :param edit_text: initial text for editing, type (bytes or str) must match the text in the caption
85
+ :type edit_text: bytes or str
87
86
  :param multiline: True: 'enter' inserts newline False: return it
88
87
  :type multiline: bool
89
88
  :param align: typically 'left', 'center' or 'right'
@@ -97,7 +96,7 @@ class Edit(Text):
97
96
  :param layout: defaults to a shared :class:`StandardTextLayout` instance
98
97
  :type layout: text layout instance
99
98
  :param mask: hide text entered with this character, None:disable mask
100
- :type mask: bytes or unicode
99
+ :type mask: bytes or str
101
100
 
102
101
  >>> Edit()
103
102
  <Edit selectable flow widget '' edit_pos=0>
@@ -108,33 +107,60 @@ class Edit(Text):
108
107
  >>> Edit("", "3.14", align="right")
109
108
  <Edit selectable flow widget '3.14' align='right' edit_pos=4>
110
109
  """
111
-
112
- super().__init__("", align, wrap, layout)
110
+ super().__init__(Text("", align, wrap, layout))
113
111
  self.multiline = multiline
114
112
  self.allow_tab = allow_tab
115
113
  self._edit_pos = 0
116
- self._caption, self._attrib = decompose_tagmarkup(caption) # type: ignore[arg-type] # discourage `bytes` input
114
+ self._caption, self._attrib = decompose_tagmarkup(caption) # type: ignore[arg-type]
117
115
  self._edit_text = ""
118
116
  self.highlight: tuple[int, int] | None = None
117
+ self._mask: str | None = None
118
+ self._shift_view_to_cursor = False
119
+ self.pref_col_maxcol: tuple[int | None, int | None] = (None, None)
119
120
  self.set_edit_text(edit_text)
120
121
  if edit_pos is None:
121
122
  edit_pos = len(edit_text)
122
123
  self.set_edit_pos(edit_pos)
123
124
  self.set_mask(mask)
124
- self._shift_view_to_cursor = False
125
+
126
+ def selectable(self) -> bool:
127
+ """Selectable mark.
128
+
129
+ Wrapped Text widget is not selectable while the edit widget has to be selectable.
130
+ """
131
+ return True
132
+
133
+ def sizing(self) -> frozenset[Sizing]:
134
+ """Sizing information.
135
+
136
+ Edit widget sizing is narrower than Text: we cannot predict the final input length.
137
+ """
138
+ return self._sizing
125
139
 
126
140
  def _repr_words(self) -> list[str]:
127
- return (
128
- super()._repr_words()[:-1]
129
- + [repr(self._edit_text)]
130
- + [f"caption={self._caption!r}"] * bool(self._caption)
131
- + ["multiline"] * (self.multiline is True)
132
- )
141
+ return [
142
+ *super()._repr_words(),
143
+ repr(self._edit_text),
144
+ *([f"caption={self._caption!r}"] if self._caption else []),
145
+ *(["multiline"] if self.multiline is True else []),
146
+ ]
133
147
 
134
148
  def _repr_attrs(self) -> dict[str, typing.Any]:
135
- attrs = {**super()._repr_attrs(), "edit_pos": self._edit_pos}
149
+ attrs = {
150
+ **super()._repr_attrs(),
151
+ "align": self._w.align,
152
+ "wrap": self._w.wrap,
153
+ "edit_pos": self._edit_pos,
154
+ }
136
155
  return remove_defaults(attrs, Edit.__init__)
137
156
 
157
+ def _sync_wrapped(self) -> None:
158
+ """Synchronize the wrapped Text widget with current caption+edit_text state."""
159
+ text, attrib = self.get_text()
160
+ self._w._text = text # pylint: disable=protected-access
161
+ self._w._attrib = attrib # pylint: disable=protected-access
162
+ self._w._invalidate()
163
+
138
164
  def get_text(self) -> tuple[str | bytes, list[tuple[Hashable, int]]]:
139
165
  """
140
166
  Returns ``(text, display attributes)``. See :meth:`Text.get_text`
@@ -151,27 +177,59 @@ class Edit(Text):
151
177
  """
152
178
 
153
179
  if self._mask is None:
154
- return self._caption + self._edit_text, self._attrib
180
+ return self._caption + self._edit_text, self._attrib # type: ignore[operator] # type normalised
155
181
 
156
- return self._caption + (self._mask * len(self._edit_text)), self._attrib
182
+ return self._caption + (self._mask * len(self._edit_text)), self._attrib # type: ignore[operator]
157
183
 
158
- def set_text(self, markup: _TagMarkup) -> None:
184
+ @property
185
+ def text(self) -> str | bytes:
159
186
  """
160
- Not supported by Edit widget.
187
+ Read-only property returning the complete bytes/unicode content
188
+ of this widget (caption plus edit_text, possibly masked).
189
+ """
190
+ return self.get_text()[0]
161
191
 
162
- >>> Edit().set_text("test")
163
- Traceback (most recent call last):
164
- EditError: set_text() not supported. Use set_caption() or set_edit_text() instead.
192
+ @property
193
+ def attrib(self) -> list[tuple[Hashable, int]]:
194
+ """
195
+ Read-only property returning the run-length encoded display
196
+ attributes of this widget.
165
197
  """
166
- # FIXME: this smells. reimplement Edit as a WidgetWrap subclass to
167
- # clean this up
198
+ return self.get_text()[1]
168
199
 
169
- # hack to let Text.__init__() work
170
- if not hasattr(self, "_text") and markup == "": # noqa: PLC1901,RUF100
171
- self._text = None
172
- return
200
+ def set_align_mode(self, mode: Literal["left", "center", "right"] | Align) -> None:
201
+ """
202
+ Set text alignment mode. See :meth:`Text.set_align_mode` for details.
203
+ """
204
+ self._w.set_align_mode(mode)
205
+ self._invalidate()
173
206
 
174
- raise EditError("set_text() not supported. Use set_caption() or set_edit_text() instead.")
207
+ def set_wrap_mode(self, mode: Literal["space", "any", "clip", "ellipsis"] | WrapMode) -> None:
208
+ """
209
+ Set text wrapping mode. See :meth:`Text.set_wrap_mode` for details.
210
+ """
211
+ self._w.set_wrap_mode(mode)
212
+ self._invalidate()
213
+
214
+ def set_layout(
215
+ self,
216
+ align: Literal["left", "center", "right"] | Align,
217
+ wrap: Literal["space", "any", "clip", "ellipsis"] | WrapMode,
218
+ layout: text_layout.TextLayout | None = None,
219
+ ) -> None:
220
+ """
221
+ Set the text layout object, alignment and wrapping modes at the same time.
222
+ See :meth:`Text.set_layout` for details.
223
+ """
224
+ self._w.set_layout(align, wrap, layout)
225
+ self._invalidate()
226
+
227
+ align = property(lambda self: self._w.align, set_align_mode)
228
+ wrap = property(lambda self: self._w.wrap, set_wrap_mode)
229
+
230
+ @property
231
+ def layout(self) -> text_layout.TextLayout:
232
+ return self._w.layout
175
233
 
176
234
  def get_pref_col(self, size: tuple[int]) -> int:
177
235
  """
@@ -209,7 +267,7 @@ class Edit(Text):
209
267
  if then_maxcol != maxcol:
210
268
  return self.get_cursor_coords((maxcol,))[0]
211
269
 
212
- return pref_col
270
+ return typing.cast("int", pref_col)
213
271
 
214
272
  def set_caption(self, caption: str | tuple[Hashable, str] | list[str | tuple[Hashable, str]]) -> None:
215
273
  """
@@ -231,11 +289,12 @@ class Edit(Text):
231
289
  Traceback (most recent call last):
232
290
  AttributeError: can't set attribute
233
291
  """
234
- self._caption, self._attrib = decompose_tagmarkup(caption)
292
+ self._caption, self._attrib = decompose_tagmarkup(caption) # type: ignore[arg-type]
293
+ self._sync_wrapped()
235
294
  self._invalidate()
236
295
 
237
296
  @property
238
- def caption(self) -> str:
297
+ def caption(self) -> str | bytes:
239
298
  """
240
299
  Read-only property returning the caption for this widget.
241
300
  """
@@ -285,6 +344,7 @@ class Edit(Text):
285
344
  """
286
345
 
287
346
  self._mask = mask
347
+ self._sync_wrapped()
288
348
  self._invalidate()
289
349
 
290
350
  def set_edit_text(self, text: str) -> None:
@@ -310,6 +370,7 @@ class Edit(Text):
310
370
  self._emit("change", text)
311
371
  old_text = self._edit_text
312
372
  self._edit_text = text
373
+ self._sync_wrapped()
313
374
  self.edit_pos = min(self.edit_pos, len(text))
314
375
 
315
376
  self._emit("postchange", old_text)
@@ -367,8 +428,8 @@ class Edit(Text):
367
428
  if tu == cu:
368
429
  return text
369
430
  if tu:
370
- return text.encode("ascii") # follow python2's implicit conversion
371
- return text.decode("ascii")
431
+ return text.encode("ascii") # type: ignore[union-attr] # follow python2's implicit conversion
432
+ return text.decode("ascii") # type: ignore[union-attr]
372
433
 
373
434
  def insert_text_result(self, text: str) -> tuple[str | bytes, int]:
374
435
  """
@@ -401,7 +462,7 @@ class Edit(Text):
401
462
 
402
463
  def keypress(
403
464
  self,
404
- size: tuple[int], # type: ignore[override]
465
+ size: tuple[int],
405
466
  key: str,
406
467
  ) -> str | None:
407
468
  """
@@ -460,9 +521,6 @@ class Edit(Text):
460
521
  if pref_col is None:
461
522
  raise ValueError(pref_col)
462
523
 
463
- # if pref_col is None:
464
- # pref_col = x
465
-
466
524
  if self._command_map[key] == Command.UP:
467
525
  y -= 1
468
526
  else:
@@ -546,7 +604,7 @@ class Edit(Text):
546
604
 
547
605
  def mouse_event(
548
606
  self,
549
- size: tuple[int], # type: ignore[override]
607
+ size: tuple[int],
550
608
  event: str,
551
609
  button: int,
552
610
  col: int,
@@ -581,9 +639,9 @@ class Edit(Text):
581
639
  self.highlight = None
582
640
  return True
583
641
 
584
- def render( # type: ignore[override]
642
+ def render(
585
643
  self,
586
- size: tuple[int], # type: ignore[override]
644
+ size: tuple[int],
587
645
  focus: bool = False,
588
646
  ) -> TextCanvas | CompositeCanvas:
589
647
  """
@@ -599,7 +657,11 @@ class Edit(Text):
599
657
  """
600
658
  self._shift_view_to_cursor = bool(focus)
601
659
 
602
- canv: TextCanvas | CompositeCanvas = super().render(size, focus)
660
+ (maxcol,) = size
661
+ text, attr = self.get_text()
662
+ trans = self.get_line_translation(maxcol, (text, attr))
663
+ canv: TextCanvas | CompositeCanvas = apply_text_layout(text, attr, trans, maxcol)
664
+
603
665
  if focus:
604
666
  canv = CompositeCanvas(canv)
605
667
  canv.cursor = self.get_cursor_coords(size)
@@ -615,7 +677,7 @@ class Edit(Text):
615
677
  maxcol: int,
616
678
  ta: tuple[str | bytes, list[tuple[Hashable, int]]] | None = None,
617
679
  ) -> list[list[tuple[int, int, int | bytes] | tuple[int, int | None]]]:
618
- trans = super().get_line_translation(maxcol, ta)
680
+ trans = self._w.get_line_translation(maxcol, ta)
619
681
  if not self._shift_view_to_cursor:
620
682
  return trans
621
683
 
@@ -685,7 +747,7 @@ class IntEdit(Edit):
685
747
 
686
748
  def keypress(
687
749
  self,
688
- size: tuple[int], # type: ignore[override]
750
+ size: tuple[int],
689
751
  key: str,
690
752
  ) -> str | None:
691
753
  """