meltygui 0.1.0__py3-none-any.whl

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 (372) hide show
  1. meltygui/__init__.py +107 -0
  2. meltygui/accounts/__init__.py +0 -0
  3. meltygui/accounts/internet_accounts.py +1355 -0
  4. meltygui/chat/__init__.py +91 -0
  5. meltygui/chat/activity.py +75 -0
  6. meltygui/chat/backends.py +36 -0
  7. meltygui/chat/chat_interface.py +732 -0
  8. meltygui/chat/chat_proxy.py +352 -0
  9. meltygui/chat/codex_proxy.py +592 -0
  10. meltygui/chat/codex_settings.py +100 -0
  11. meltygui/chat/codex_transport.py +60 -0
  12. meltygui/chat/command_parser.py +204 -0
  13. meltygui/chat/images.py +227 -0
  14. meltygui/chat/messages.py +417 -0
  15. meltygui/chat/metadata.py +139 -0
  16. meltygui/chat/writer_locks.py +64 -0
  17. meltygui/code/__init__.py +0 -0
  18. meltygui/code/basic_converters.py +533 -0
  19. meltygui/code/chain_converters.py +2111 -0
  20. meltygui/code/code_checks.py +2209 -0
  21. meltygui/code/core_syntax.py +1430 -0
  22. meltygui/code/file_converters.py +1933 -0
  23. meltygui/code/fileref.py +702 -0
  24. meltygui/code/hotswap_guard.py +144 -0
  25. meltygui/code/libcst_conversion.py +9724 -0
  26. meltygui/code/live_instrument.py +392 -0
  27. meltygui/code/live_view.py +2490 -0
  28. meltygui/code/melty_scan.py +2684 -0
  29. meltygui/code/new_codecs.py +1255 -0
  30. meltygui/code/new_converters.py +3017 -0
  31. meltygui/code/project_code.py +278 -0
  32. meltygui/code/source_context.py +63 -0
  33. meltygui/code/symbol_roster.py +1588 -0
  34. meltygui/code/syntax_check.py +34 -0
  35. meltygui/code/syntax_check_worker.py +114 -0
  36. meltygui/completion/__init__.py +0 -0
  37. meltygui/completion/fim.py +1232 -0
  38. meltygui/completion/fim_context.py +481 -0
  39. meltygui/completion/providers/__init__.py +0 -0
  40. meltygui/completion/providers/anthropic_oauth.py +446 -0
  41. meltygui/completion/providers/anthropic_requests.py +69 -0
  42. meltygui/completion/providers/claude.py +203 -0
  43. meltygui/completion/providers/claude_usage.py +499 -0
  44. meltygui/completion/providers/codex_accounts.py +180 -0
  45. meltygui/completion/providers/copilot.py +617 -0
  46. meltygui/completion/providers/oauth_popup.py +220 -0
  47. meltygui/completion/providers/ollama.py +320 -0
  48. meltygui/completion/providers/profiles.py +23 -0
  49. meltygui/core/README.md +88 -0
  50. meltygui/core/__init__.py +1 -0
  51. meltygui/core/automation/__init__.py +1 -0
  52. meltygui/core/automation/action_core.py +153 -0
  53. meltygui/core/automation/collection_action.py +45 -0
  54. meltygui/core/automation/mcp_eval.py +167 -0
  55. meltygui/core/automation/mcp_hotswap.py +107 -0
  56. meltygui/core/automation/mcp_query.py +552 -0
  57. meltygui/core/automation/mcp_server.py +657 -0
  58. meltygui/core/automation/orchestration_core.py +2636 -0
  59. meltygui/core/automation/query_core.py +124 -0
  60. meltygui/core/automation/search_core.py +26 -0
  61. meltygui/core/automation/selector_core.py +340 -0
  62. meltygui/core/automation/value_core.py +1752 -0
  63. meltygui/core/cache/__init__.py +1 -0
  64. meltygui/core/cache/cache_diagnostics.py +0 -0
  65. meltygui/core/cache/invalidation_decoration.py +153 -0
  66. meltygui/core/cache/invalidation_tracker.py +43 -0
  67. meltygui/core/cache/tile_cache.py +5968 -0
  68. meltygui/core/conversion/__init__.py +1 -0
  69. meltygui/core/conversion/bubbling.py +599 -0
  70. meltygui/core/conversion/cache_tree.py +188 -0
  71. meltygui/core/conversion/chain.py +113 -0
  72. meltygui/core/conversion/converter_register.py +145 -0
  73. meltygui/core/conversion/data_decoration.py +67 -0
  74. meltygui/core/conversion/dict_conversion.py +1815 -0
  75. meltygui/core/conversion/dict_conversion_util.py +177 -0
  76. meltygui/core/conversion/dynamic_obj.py +89 -0
  77. meltygui/core/conversion/graph_compare.py +183 -0
  78. meltygui/core/conversion/load_save_v2.py +1032 -0
  79. meltygui/core/conversion/missing_saved_class.py +39 -0
  80. meltygui/core/conversion/path_finder.py +606 -0
  81. meltygui/core/conversion/render_host.py +1083 -0
  82. meltygui/core/core_render.py +6537 -0
  83. meltygui/core/definition_hotswap.py +231 -0
  84. meltygui/core/diagnostics/__init__.py +1 -0
  85. meltygui/core/diagnostics/attribute_churn.py +38 -0
  86. meltygui/core/diagnostics/fps_counter.py +39 -0
  87. meltygui/core/diagnostics/gpu_frame_timer.py +103 -0
  88. meltygui/core/diagnostics/inspection_core.py +169 -0
  89. meltygui/core/diagnostics/monitor_core.py +105 -0
  90. meltygui/core/diagnostics/notifications.py +706 -0
  91. meltygui/core/diagnostics/perf_trace.py +281 -0
  92. meltygui/core/diagnostics/profile_decoration.py +88 -0
  93. meltygui/core/diagnostics/resize_trace.py +62 -0
  94. meltygui/core/diagnostics/screenshot_core.py +247 -0
  95. meltygui/core/diagnostics/session_status.py +98 -0
  96. meltygui/core/diagnostics/trace_core.py +445 -0
  97. meltygui/core/files/__init__.py +1 -0
  98. meltygui/core/files/file_core.py +208 -0
  99. meltygui/core/files/file_explorer_core.py +104 -0
  100. meltygui/core/files/file_tree_core.py +198 -0
  101. meltygui/core/files/file_watch_core.py +43 -0
  102. meltygui/core/files/import_graph_core.py +43 -0
  103. meltygui/core/files/metadata_core.py +51 -0
  104. meltygui/core/graphics/__init__.py +1 -0
  105. meltygui/core/graphics/cuda_context_core.py +166 -0
  106. meltygui/core/graphics/cuda_interop_core.py +136 -0
  107. meltygui/core/graphics/cuda_kernel_core.py +91 -0
  108. meltygui/core/graphics/framebuffer_recorder.py +337 -0
  109. meltygui/core/graphics/gl_state.py +658 -0
  110. meltygui/core/graphics/lut_core.py +52 -0
  111. meltygui/core/graphics/overlay_renderer.py +984 -0
  112. meltygui/core/graphics/scene_target.py +180 -0
  113. meltygui/core/graphics/screenshot.py +439 -0
  114. meltygui/core/graphics/shader_func.py +478 -0
  115. meltygui/core/graphics/tensor_core.py +45 -0
  116. meltygui/core/graphics/text_texture.py +329 -0
  117. meltygui/core/graphics/wayland_color.py +635 -0
  118. meltygui/core/input/__init__.py +1 -0
  119. meltygui/core/input/collision.py +165 -0
  120. meltygui/core/input/drag_drop_core.py +1525 -0
  121. meltygui/core/input/hypr_left_drag.py +323 -0
  122. meltygui/core/input/input_core.py +245 -0
  123. meltygui/core/input/input_handler.py +1101 -0
  124. meltygui/core/input/mouse_cursor.py +355 -0
  125. meltygui/core/input/pynput_backend.py +1054 -0
  126. meltygui/core/input/space_mouse.py +338 -0
  127. meltygui/core/input/touchpad_backend.py +393 -0
  128. meltygui/core/input/view_selection.py +177 -0
  129. meltygui/core/layout/__init__.py +1 -0
  130. meltygui/core/layout/column_core.py +2153 -0
  131. meltygui/core/layout/cursor_core.py +161 -0
  132. meltygui/core/layout/dropdown_core.py +278 -0
  133. meltygui/core/layout/edge_constraints.py +155 -0
  134. meltygui/core/layout/grid_core.py +117 -0
  135. meltygui/core/layout/header_core.py +23 -0
  136. meltygui/core/layout/header_runtime.py +67 -0
  137. meltygui/core/layout/layout_core.py +87 -0
  138. meltygui/core/layout/tile_manager_core.py +591 -0
  139. meltygui/core/melty.py +6726 -0
  140. meltygui/core/module_map.json +896 -0
  141. meltygui/core/module_names.py +19 -0
  142. meltygui/core/rendering/__init__.py +1 -0
  143. meltygui/core/rendering/core_decoration.py +431 -0
  144. meltygui/core/rendering/core_render_helpers.py +328 -0
  145. meltygui/core/rendering/func_metadata.py +398 -0
  146. meltygui/core/rendering/mode.py +818 -0
  147. meltygui/core/rendering/mode_defaults.py +41 -0
  148. meltygui/core/rendering/modes.py +136 -0
  149. meltygui/core/rendering/parameter_core.py +1665 -0
  150. meltygui/core/rendering/render_dispatch.py +1891 -0
  151. meltygui/core/rendering/render_funcs.py +273 -0
  152. meltygui/core/rendering/shaped.py +312 -0
  153. meltygui/core/rendering/window_decoration.py +25 -0
  154. meltygui/core/runtime/__init__.py +1 -0
  155. meltygui/core/runtime/app.py +735 -0
  156. meltygui/core/runtime/app_session.py +140 -0
  157. meltygui/core/runtime/background.py +564 -0
  158. meltygui/core/runtime/extensions.py +53 -0
  159. meltygui/core/runtime/gc_manager.py +1163 -0
  160. meltygui/core/runtime/lifecycle.py +21 -0
  161. meltygui/core/runtime/paths.py +27 -0
  162. meltygui/core/runtime/settings.py +14 -0
  163. meltygui/core/runtime/singleton.py +16 -0
  164. meltygui/core/runtime/thread_safe_bool.py +24 -0
  165. meltygui/core/runtime/thread_signal.py +30 -0
  166. meltygui/core/runtime/toggles.py +3115 -0
  167. meltygui/core/services/__init__.py +1 -0
  168. meltygui/core/services/account_core.py +10 -0
  169. meltygui/core/services/chat_core.py +19 -0
  170. meltygui/core/services/claude_terminal_core.py +346 -0
  171. meltygui/core/services/terminal_core.py +458 -0
  172. meltygui/core/services/terminal_runtime.py +78 -0
  173. meltygui/core/styling/__init__.py +1 -0
  174. meltygui/core/styling/color_core.py +46 -0
  175. meltygui/core/styling/fonts.py +639 -0
  176. meltygui/core/styling/global_style.py +338 -0
  177. meltygui/core/styling/style.py +198 -0
  178. meltygui/core/styling/style_core.py +522 -0
  179. meltygui/core/styling/warm_start.py +149 -0
  180. meltygui/core/windowing/__init__.py +1 -0
  181. meltygui/core/windowing/backends/PYIMGUI_LICENSE +28 -0
  182. meltygui/core/windowing/backends/__init__.py +1 -0
  183. meltygui/core/windowing/backends/imgui_renderer.py +138 -0
  184. meltygui/core/windowing/backends/native_wayland.py +850 -0
  185. meltygui/core/windowing/backends/protocols/xdg-decoration-unstable-v1.xml +156 -0
  186. meltygui/core/windowing/backends/protocols/xdg-shell.xml +1420 -0
  187. meltygui/core/windowing/backends/wayland_protocol.py +101 -0
  188. meltygui/core/windowing/dock_core.py +182 -0
  189. meltygui/core/windowing/frame_geometry.py +42 -0
  190. meltygui/core/windowing/geometry_feed.py +864 -0
  191. meltygui/core/windowing/glfw_utils.py +1343 -0
  192. meltygui/core/windowing/os_frame.py +1552 -0
  193. meltygui/core/windowing/surface.py +643 -0
  194. meltygui/core/windowing/titlebar.py +1560 -0
  195. meltygui/core/windowing/titlebar_buttons.py +281 -0
  196. meltygui/core/windowing/wayland_move.py +932 -0
  197. meltygui/core/windowing/window_api.py +62 -0
  198. meltygui/core/windowing/window_constants.py +339 -0
  199. meltygui/core/windowing/window_visibility.py +162 -0
  200. meltygui/debug/__init__.py +0 -0
  201. meltygui/debug/app_view_utils.py +9 -0
  202. meltygui/editor/__init__.py +0 -0
  203. meltygui/editor/bash_syntax.py +30 -0
  204. meltygui/editor/code_line_fast.py +152 -0
  205. meltygui/editor/diff.py +139 -0
  206. meltygui/editor/external_changes.py +159 -0
  207. meltygui/editor/file_header.py +41 -0
  208. meltygui/editor/live_usage.py +169 -0
  209. meltygui/editor/live_view_views.py +1803 -0
  210. meltygui/editor/pending_save.py +1351 -0
  211. meltygui/editor/roster_tints.py +547 -0
  212. meltygui/editor/source_preview.py +16 -0
  213. meltygui/editor/source_tools.py +10 -0
  214. meltygui/editor/source_ui.py +70 -0
  215. meltygui/editor/spell_check.py +77 -0
  216. meltygui/editor/text_editor.py +9076 -0
  217. meltygui/editor/usage_picker.py +538 -0
  218. meltygui/events/__init__.py +0 -0
  219. meltygui/events/example.py +118 -0
  220. meltygui/examples/__init__.py +0 -0
  221. meltygui/examples/columns_demo.py +33 -0
  222. meltygui/examples/columns_window_demo.py +60 -0
  223. meltygui/examples/context_menu_demo.py +24 -0
  224. meltygui/examples/context_menu_window_demo.py +49 -0
  225. meltygui/examples/gui_playground.py +127 -0
  226. meltygui/examples/live_view_playground.py +256 -0
  227. meltygui/examples/lora.py +43 -0
  228. meltygui/examples/lora_data.py +59 -0
  229. meltygui/examples/lora_policies.py +67 -0
  230. meltygui/examples/mode_demo.py +49 -0
  231. meltygui/examples/modifies_demo.py +119 -0
  232. meltygui/examples/scalar_policies.py +60 -0
  233. meltygui/examples/style_layouts.py +149 -0
  234. meltygui/examples/tile_manager_demo.py +74 -0
  235. meltygui/examples/tint_demo.py +160 -0
  236. meltygui/examples/tint_functions.py +56 -0
  237. meltygui/examples/trace_demo.py +88 -0
  238. meltygui/examples/two_windows.py +36 -0
  239. meltygui/files/__init__.py +0 -0
  240. meltygui/files/fast_file_explorer.py +439 -0
  241. meltygui/gnome_extension/lsd-window-geometry@latent-descent/extension.js +237 -0
  242. meltygui/gnome_extension/lsd-window-geometry@latent-descent/lsd-window-geometry@latent-descent.iml +9 -0
  243. meltygui/gnome_extension/lsd-window-geometry@latent-descent/metadata.json +7 -0
  244. meltygui/graphics/__init__.py +6 -0
  245. meltygui/graphics/base.py +85 -0
  246. meltygui/graphics/examples.py +507 -0
  247. meltygui/graphics/executor.py +520 -0
  248. meltygui/graphics/filter.py +667 -0
  249. meltygui/graphics/filter.pyi +856 -0
  250. meltygui/graphics/generate_stubs.py +22 -0
  251. meltygui/graphics/registry.py +203 -0
  252. meltygui/graphics/shader_compiler.py +155 -0
  253. meltygui/graphics/shaders.py +1302 -0
  254. meltygui/graphics/stub_generator.py +250 -0
  255. meltygui/graphics/texture_manager.py +170 -0
  256. meltygui/graphics/texture_min_max.py +295 -0
  257. meltygui/hdr_color.py +757 -0
  258. meltygui/image_load.py +308 -0
  259. meltygui/model/__init__.py +1 -0
  260. meltygui/model/account_model.py +142 -0
  261. meltygui/model/camera_model.py +159 -0
  262. meltygui/model/chat_model.py +86 -0
  263. meltygui/model/code_model.py +62 -0
  264. meltygui/model/code_proxy_model.py +876 -0
  265. meltygui/model/collection_model.py +41 -0
  266. meltygui/model/color_model.py +127 -0
  267. meltygui/model/cuda_tensor_model.py +36 -0
  268. meltygui/model/cuda_texture_model.py +149 -0
  269. meltygui/model/dropdown_model.py +137 -0
  270. meltygui/model/file_metadata_model.py +73 -0
  271. meltygui/model/file_model.py +289 -0
  272. meltygui/model/format_model.py +390 -0
  273. meltygui/model/graph_model.py +98 -0
  274. meltygui/model/icon_model.py +1024 -0
  275. meltygui/model/import_graph_model.py +468 -0
  276. meltygui/model/layout_model.py +15 -0
  277. meltygui/model/lut_model.py +266 -0
  278. meltygui/model/search_model.py +173 -0
  279. meltygui/model/tensor_model.py +402 -0
  280. meltygui/model/terminal_model.py +329 -0
  281. meltygui/model/texture_model.py +146 -0
  282. meltygui/model/tile_model.py +24 -0
  283. meltygui/model/trace_model.py +198 -0
  284. meltygui/model/trace_report_model.py +146 -0
  285. meltygui/models/__init__.py +0 -0
  286. meltygui/models/file_meta.py +613 -0
  287. meltygui/models/function_console.py +184 -0
  288. meltygui/models/orchestration.py +48 -0
  289. meltygui/pbr.py +1576 -0
  290. meltygui/png_unfilter.c +49 -0
  291. meltygui/resources/JetBrainsMono-Regular.ttf +0 -0
  292. meltygui/resources/THIRD_PARTY_NOTICES.md +21 -0
  293. meltygui/resources/dejavu/DejaVuSans-Bold.ttf +0 -0
  294. meltygui/resources/dejavu/DejaVuSans-ExtraLight.ttf +0 -0
  295. meltygui/resources/dejavu/DejaVuSans.ttf +0 -0
  296. meltygui/resources/dejavu/LICENSE.txt +78 -0
  297. meltygui/resources/fontawesome-LICENSE.txt +121 -0
  298. meltygui/resources/fontawesome-webfont.ttf +0 -0
  299. meltygui/resources/hdri/studio_small_09_1k.hdr +0 -0
  300. meltygui/resources/jetbrains-weights/JetBrainsMono-Bold.ttf +0 -0
  301. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraBold.ttf +0 -0
  302. meltygui/resources/jetbrains-weights/JetBrainsMono-ExtraLight.ttf +0 -0
  303. meltygui/resources/jetbrains-weights/JetBrainsMono-Light.ttf +0 -0
  304. meltygui/resources/jetbrains-weights/JetBrainsMono-Medium.ttf +0 -0
  305. meltygui/resources/jetbrains-weights/JetBrainsMono-SemiBold.ttf +0 -0
  306. meltygui/resources/jetbrains-weights/JetBrainsMono-Thin.ttf +0 -0
  307. meltygui/resources/jetbrains-weights/OFL.txt +93 -0
  308. meltygui/resources/jetbrains-weights/README.md +2 -0
  309. meltygui/state/__init__.py +0 -0
  310. meltygui/state/account_state.py +22 -0
  311. meltygui/state/animation_state.py +97 -0
  312. meltygui/state/annotation_state.py +22 -0
  313. meltygui/state/chat_state.py +36 -0
  314. meltygui/state/code_state.py +10 -0
  315. meltygui/state/core_enums.py +59 -0
  316. meltygui/state/core_markers.py +115 -0
  317. meltygui/state/core_undo.py +1075 -0
  318. meltygui/state/file_state.py +75 -0
  319. meltygui/state/graph_state.py +26 -0
  320. meltygui/state/inspection_state.py +47 -0
  321. meltygui/state/menu_state.py +10 -0
  322. meltygui/state/model_enums.py +11 -0
  323. meltygui/state/new_core_model.py +2394 -0
  324. meltygui/state/orchestration_state.py +14 -0
  325. meltygui/state/query_state.py +24 -0
  326. meltygui/state/tensor_state.py +10 -0
  327. meltygui/state/terminal_state.py +23 -0
  328. meltygui/state/trace_state.py +32 -0
  329. meltygui/state/voxel_state.py +12 -0
  330. meltygui/text_index.py +816 -0
  331. meltygui/utils/__init__.py +0 -0
  332. meltygui/utils/jump_to_code.py +344 -0
  333. meltygui/utils/pkl_inspect.py +90 -0
  334. meltygui/utils/render_utils.py +1419 -0
  335. meltygui/view/__init__.py +1 -0
  336. meltygui/view/account_view.py +524 -0
  337. meltygui/view/action_view.py +73 -0
  338. meltygui/view/chat_decoration_view.py +112 -0
  339. meltygui/view/chat_view.py +1703 -0
  340. meltygui/view/code_view.py +2677 -0
  341. meltygui/view/collection_view.py +1185 -0
  342. meltygui/view/color_view.py +824 -0
  343. meltygui/view/control_view.py +559 -0
  344. meltygui/view/decoration_view.py +439 -0
  345. meltygui/view/diagnostic_view.py +177 -0
  346. meltygui/view/dropdown_view.py +1082 -0
  347. meltygui/view/file_view.py +1599 -0
  348. meltygui/view/graph_cuda_view.py +105 -0
  349. meltygui/view/graph_view.py +882 -0
  350. meltygui/view/header_view.py +848 -0
  351. meltygui/view/input_view.py +238 -0
  352. meltygui/view/inspection_view.py +1783 -0
  353. meltygui/view/layout_view.py +596 -0
  354. meltygui/view/lut_view.py +45 -0
  355. meltygui/view/menu_view.py +212 -0
  356. meltygui/view/orchestration_view.py +658 -0
  357. meltygui/view/query_view.py +118 -0
  358. meltygui/view/search_view.py +342 -0
  359. meltygui/view/tab_view.py +258 -0
  360. meltygui/view/tensor_view.py +430 -0
  361. meltygui/view/terminal_view.py +355 -0
  362. meltygui/view/text_view.py +8031 -0
  363. meltygui/view/texture_view.py +480 -0
  364. meltygui/view/tile_view.py +38 -0
  365. meltygui/view/trace_view.py +923 -0
  366. meltygui/view/voxel_cuda_view.py +824 -0
  367. meltygui/view/voxel_view.py +1860 -0
  368. meltygui/view/window_view.py +111 -0
  369. meltygui-0.1.0.dist-info/METADATA +143 -0
  370. meltygui-0.1.0.dist-info/RECORD +372 -0
  371. meltygui-0.1.0.dist-info/WHEEL +4 -0
  372. meltygui-0.1.0.dist-info/licenses/LICENSE +202 -0
@@ -0,0 +1,393 @@
1
+ """
2
+ OS-level three-finger click/drag for Linux touchpads (macOS-style).
3
+
4
+ Architecture — permanent grab + clone forwarding:
5
+
6
+ real touchpad ──(EVIOCGRAB, ours exclusively)──► this daemon
7
+ │ not latched: every event forwarded verbatim ──► virtual clone
8
+ │ (libinput drives the cursor from the clone as normal)
9
+ └─ latched (3-finger): events consumed; finger motion becomes
10
+ REL_X/REL_Y + BTN_MIDDLE on a separate virtual pointer
11
+
12
+ libinput only ever sees the clone, whose state we keep consistent: when the
13
+ latch begins we synthesize a clean "all fingers lifted" frame on the clone,
14
+ so there is never a mid-gesture grab/ungrab that strands libinput with
15
+ phantom fingers (which froze pointer input in the grab-on-demand version).
16
+
17
+ Safety — the system must NEVER lose the mouse:
18
+ - The kernel releases EVIOCGRAB automatically when our fd closes, for any
19
+ exit including SIGKILL. A crash always returns the real touchpad.
20
+ - Finger count comes from the hardware's own BTN_TOOL_* / BTN_TOUCH keys
21
+ (authoritative), not our MT-slot bookkeeping, so the latch cannot stick
22
+ on a miscount; BTN_TOUCH==0 force-releases regardless of other state.
23
+ - A physical clickpad button press while latched is an emergency release
24
+ (and the click passes through to the virtual pointer, so it still works).
25
+ - A watchdog force-releases the latch (and, on repeated trouble, shuts the
26
+ whole daemon down, closing fds → grab released) if the device goes
27
+ silent mid-latch for WATCHDOG_S seconds.
28
+ - Any exception path closes all fds in `finally`.
29
+
30
+ Gesture semantics (macOS "three finger drag"): middle press immediately on
31
+ 3-finger contact (a quick tap is a middle click); lifting down to one finger
32
+ keeps the latch; releasing all fingers releases the button.
33
+
34
+ The virtual pointer declares BTN_LEFT/BTN_RIGHT alongside BTN_MIDDLE —
35
+ libinput refuses to treat a device without BTN_LEFT as a pointer, which is
36
+ why the first version's middle clicks never reached the compositor.
37
+ """
38
+
39
+ from __future__ import annotations
40
+ import threading
41
+ import time
42
+
43
+ try:
44
+ import evdev
45
+ from evdev import InputDevice, UInput, ecodes as e
46
+ HAS_EVDEV = True
47
+ except ImportError:
48
+ HAS_EVDEV = False
49
+
50
+ # Touchpad units → cursor pixels (pad ~20 units/mm).
51
+ SENSITIVITY = 0.55
52
+ ACCEL_MAX = 2.5
53
+ ACCEL_REF = 30.0 # units/s where acceleration saturates
54
+ WATCHDOG_S = 5.0 # latched + zero events for this long → force release
55
+ # Fingers land staggered (1→2→3 over a few frames). Events from touch-begin
56
+ # are held back: reach 3 fingers → latch and the clone never sees the touch;
57
+ # otherwise the buffer flushes and the touch proceeds normally. Before this
58
+ # the clone saw a brief 2-finger touch + our synthesized lift, which libinput
59
+ # read as a 2-finger tap → spurious RIGHT CLICKS.
60
+ # The flush trigger is MOTION, not time: a natural 3-finger touch often
61
+ # starts from 1–2 resting fingers with the last finger landing 200ms+ later,
62
+ # so any fixed window either turns 3-finger drags into 2-finger SCROLLS
63
+ # (flushed too early) or makes every scroll start sluggish (flushed too
64
+ # late). Stationary fingers carry no intent - hold them back indefinitely;
65
+ # actual travel (or a physical button press, or the touch ending as a tap)
66
+ # reveals the intent and flushes immediately.
67
+ MOVE_FLUSH = 15 # device units (~0.75mm) of travel that flushes the hold
68
+ DEBUG = False
69
+
70
+ _TOOL_COUNT = {}
71
+ if HAS_EVDEV:
72
+ _TOOL_COUNT = {
73
+ e.BTN_TOOL_FINGER: 1, e.BTN_TOOL_DOUBLETAP: 2,
74
+ e.BTN_TOOL_TRIPLETAP: 3, e.BTN_TOOL_QUADTAP: 4,
75
+ e.BTN_TOOL_QUINTTAP: 5,
76
+ }
77
+
78
+
79
+ CLONE_TAG = "(meltygui)"
80
+
81
+
82
+ def find_touchpad():
83
+ for path in evdev.list_devices():
84
+ try:
85
+ dev = InputDevice(path)
86
+ except OSError:
87
+ continue
88
+ # NEVER pick one of our own virtual devices: a stale clone from
89
+ # another still-running daemon (e.g. the model server's) matches the
90
+ # MT capability check and only ever carries the events that daemon
91
+ # forwards - grabbing it permanently breaks gesture detection.
92
+ if CLONE_TAG in dev.name or "latent-descent" in dev.name:
93
+ dev.close()
94
+ continue
95
+ caps = dev.capabilities()
96
+ abs_codes = {c for c, _ in caps.get(e.EV_ABS, [])}
97
+ if e.ABS_MT_SLOT in abs_codes and e.BTN_TOOL_FINGER in caps.get(e.EV_KEY, []):
98
+ return dev
99
+ dev.close()
100
+ return None
101
+
102
+
103
+ def existing_clone_present():
104
+ """A live clone means another process' daemon already owns the touchpad."""
105
+ for path in evdev.list_devices():
106
+ try:
107
+ dev = InputDevice(path)
108
+ except OSError:
109
+ continue
110
+ name = dev.name
111
+ dev.close()
112
+ if CLONE_TAG in name:
113
+ return True
114
+ return False
115
+
116
+
117
+ class ThreeFingerDrag:
118
+ def __init__(self):
119
+ self.dev: InputDevice | None = None
120
+ self.clone: UInput | None = None
121
+ self.pointer: UInput | None = None
122
+ self._thread: threading.Thread | None = None
123
+ self._stop = threading.Event()
124
+ self._latched = False
125
+ self._last_event_t = 0.0
126
+
127
+ # ------------------------------------------------------------- lifecycle
128
+
129
+ def start(self) -> bool:
130
+ if not HAS_EVDEV:
131
+ print("touchpad_backend: evdev not installed, 3-finger drag disabled")
132
+ return False
133
+ if existing_clone_present():
134
+ print("touchpad_backend: another 3-finger-drag daemon is already "
135
+ "running (clone device present) — not starting a second one")
136
+ return False
137
+ self.dev = find_touchpad()
138
+ if self.dev is None:
139
+ print("touchpad_backend: no multitouch touchpad found")
140
+ return False
141
+ try:
142
+ # BTN_LEFT is mandatory for libinput to classify this as a pointer.
143
+ self.pointer = UInput(
144
+ {e.EV_REL: [e.REL_X, e.REL_Y],
145
+ e.EV_KEY: [e.BTN_LEFT, e.BTN_RIGHT, e.BTN_MIDDLE]},
146
+ name="latent-descent virtual pointer")
147
+ # Clone of the touchpad - libinput's view of the pad from now on.
148
+ self.clone = UInput.from_device(self.dev,
149
+ name=f"{self.dev.name} (meltygui)")
150
+ # Give udev/libinput a moment to pick the clone up BEFORE we grab
151
+ # the real device, so there is no window with no working touchpad.
152
+ time.sleep(0.5)
153
+ self.dev.grab()
154
+ except Exception as ex:
155
+ print(f"touchpad_backend: setup failed ({ex}), 3-finger drag disabled")
156
+ self._close_all()
157
+ return False
158
+ self._thread = threading.Thread(target=self._run, name="touchpad-3fd",
159
+ daemon=True)
160
+ self._thread.start()
161
+ threading.Thread(target=self._watchdog, name="touchpad-3fd-watchdog",
162
+ daemon=True).start()
163
+ print(f"touchpad_backend: 3-finger drag active on {self.dev.name}")
164
+ return True
165
+
166
+ def stop(self):
167
+ self._stop.set()
168
+ self._close_all() # closing dev releases the grab and unblocks reading
169
+
170
+ def _close_all(self):
171
+ for attr in ("dev", "clone", "pointer"):
172
+ obj = getattr(self, attr)
173
+ if obj is not None:
174
+ try:
175
+ obj.close()
176
+ except Exception:
177
+ pass
178
+ setattr(self, attr, None)
179
+
180
+ def _watchdog(self):
181
+ """Absolute backstop: a latch with a silent device is force-released;
182
+ if the release itself can't be delivered, kill the daemon entirely —
183
+ closing the fds drops the grab and hands the real pad back to libinput."""
184
+ while not self._stop.is_set():
185
+ time.sleep(1.0)
186
+ if self._latched and time.monotonic() - self._last_event_t > WATCHDOG_S:
187
+ print("touchpad_backend: watchdog releasing stale 3-finger latch")
188
+ try:
189
+ self._release_latch()
190
+ except Exception:
191
+ print("touchpad_backend: watchdog release failed — "
192
+ "shutting down, touchpad returns to system")
193
+ self.stop()
194
+ return
195
+
196
+ # ------------------------------------------------------------- latch ops
197
+
198
+ def _release_latch(self):
199
+ if self._latched:
200
+ self.pointer.write(e.EV_KEY, e.BTN_MIDDLE, 0)
201
+ self.pointer.syn()
202
+ self._latched = False
203
+
204
+ def _lift_fingers_on_clone(self, touches, active_tool):
205
+ """Tell libinput (via the clone) that all fingers left the pad, so the
206
+ clone's state stays consistent while we consume the real events."""
207
+ c = self.clone
208
+ for slot in list(touches):
209
+ c.write(e.EV_ABS, e.ABS_MT_SLOT, slot)
210
+ c.write(e.EV_ABS, e.ABS_MT_TRACKING_ID, -1)
211
+ if active_tool is not None:
212
+ c.write(e.EV_KEY, active_tool, 0)
213
+ c.write(e.EV_KEY, e.BTN_TOUCH, 0)
214
+ c.syn()
215
+
216
+ # ------------------------------------------------------------- main loop
217
+
218
+ def _run(self):
219
+ dev, clone, pointer = self.dev, self.clone, self.pointer
220
+ touches: dict[int, list] = {} # slot -> [x, y]
221
+ slot = 0
222
+ fingers = 0 # from BTN_TOOL_* (authoritative)
223
+ touching = False # BTN_TOUCH level
224
+ active_tool = None
225
+ frame: list = [] # raw events of the current frame
226
+ primary: tuple | None = None # (slot, x, y) the drag follows
227
+ rx = ry = 0.0
228
+ # Touch-begin hold: frames withheld from the clone while we wait to
229
+ # see if this touch becomes 3-finger (see MOVE_FLUSH).
230
+ holding = False
231
+ hold_buf: list = [] # withheld raw events (frames + SYNs)
232
+ hold_origin: dict[int, tuple] = {} # slot -> first (x, y) seen in hold
233
+ hold_flush_now = False # physical button seen mid-hold
234
+ clone_touching = False # does the clone believe fingers are down?
235
+
236
+ def flush_hold():
237
+ nonlocal holding, clone_touching
238
+ for hev in hold_buf:
239
+ clone.write_event(hev)
240
+ if hold_buf:
241
+ clone.syn()
242
+ clone_touching = touching
243
+ hold_buf.clear()
244
+ holding = False
245
+
246
+ try:
247
+ for ev in dev.read_loop():
248
+ if self._stop.is_set():
249
+ break
250
+ self._last_event_t = time.monotonic()
251
+
252
+ if ev.type == e.EV_ABS:
253
+ if ev.code == e.ABS_MT_SLOT:
254
+ slot = ev.value
255
+ elif ev.code == e.ABS_MT_TRACKING_ID:
256
+ if ev.value == -1:
257
+ touches.pop(slot, None)
258
+ else:
259
+ touches[slot] = [None, None]
260
+ elif ev.code == e.ABS_MT_POSITION_X and slot in touches:
261
+ touches[slot][0] = ev.value
262
+ elif ev.code == e.ABS_MT_POSITION_Y and slot in touches:
263
+ touches[slot][1] = ev.value
264
+ elif ev.type == e.EV_KEY:
265
+ if ev.code in _TOOL_COUNT:
266
+ if ev.value:
267
+ fingers = _TOOL_COUNT[ev.code]
268
+ active_tool = ev.code
269
+ if DEBUG:
270
+ print(f"3fd: fingers={fingers}")
271
+ elif ev.code == active_tool:
272
+ active_tool = None
273
+ fingers = 0
274
+ elif ev.code == e.BTN_TOUCH:
275
+ if ev.value and not touching:
276
+ # Touch begin → start withholding from the clone.
277
+ holding = True
278
+ hold_buf.clear()
279
+ hold_origin.clear()
280
+ hold_flush_now = False
281
+ touching = bool(ev.value)
282
+ elif ev.code in (e.BTN_LEFT, e.BTN_RIGHT):
283
+ if self._latched:
284
+ # Physical clickpad press mid-latch: emergency
285
+ # release; the clickpad works via the pointer.
286
+ self._release_latch()
287
+ pointer.write(e.EV_KEY, ev.code, ev.value)
288
+ pointer.syn()
289
+ elif holding:
290
+ # A physical click must never be missed - flush
291
+ # the held touch so libinput sees it immediately.
292
+ hold_flush_now = True
293
+
294
+ if not (ev.type == e.EV_SYN and ev.code == e.SYN_REPORT):
295
+ frame.append(ev)
296
+ continue
297
+
298
+ # ---- end of frame: handle latch / hold / forward / consume ----
299
+ if not self._latched and fingers >= 3 and touching:
300
+ if clone_touching:
301
+ # Touch was already flushed to the clone (3rd finger
302
+ # arrived after we began) - retract it cleanly.
303
+ self._lift_fingers_on_clone(touches, active_tool)
304
+ clone_touching = False
305
+ holding = False
306
+ hold_buf.clear() # clone never sees the held prefix
307
+ pointer.write(e.EV_KEY, e.BTN_MIDDLE, 1)
308
+ pointer.syn()
309
+ self._latched = True
310
+ primary = None
311
+ rx = ry = 0.0
312
+ if DEBUG:
313
+ print("3fd: LATCH")
314
+ elif self._latched and (not touching or not touches):
315
+ self._release_latch()
316
+ if DEBUG:
317
+ print("3fd: RELEASE")
318
+ frame = [] # clone already thinks fingers are up
319
+ continue
320
+
321
+ if self._latched:
322
+ primary, rx, ry = self._track(pointer, touches, primary, rx, ry)
323
+ elif holding:
324
+ hold_buf.extend(frame)
325
+ hold_buf.append(ev) # keep the SYN so frames stay framed
326
+ # Flush when the touch reveals a non-3-finger intent: it
327
+ # ended (a tap), a physical button was pressed, or the
328
+ # fingers have traveled (cursor move / scroll).
329
+ # Stationary fingers stay held - the 3rd finger may still
330
+ # be on the way, however late.
331
+ moved = 0
332
+ for s, pos in touches.items():
333
+ if pos[0] is None or pos[1] is None:
334
+ continue
335
+ if s not in hold_origin:
336
+ hold_origin[s] = (pos[0], pos[1])
337
+ ox, oy = hold_origin[s]
338
+ moved = max(moved, abs(pos[0] - ox) + abs(pos[1] - oy))
339
+ if not touching or hold_flush_now or moved > MOVE_FLUSH:
340
+ flush_hold()
341
+ else:
342
+ for fev in frame:
343
+ clone.write_event(fev)
344
+ clone.syn()
345
+ clone_touching = touching
346
+ frame = []
347
+ except OSError:
348
+ pass # device closed / unplugged / suspend - exit cleanly
349
+ finally:
350
+ try:
351
+ self._release_latch()
352
+ except Exception:
353
+ pass
354
+ self._close_all() # closing dev releases the kernel grab
355
+
356
+ @staticmethod
357
+ def _track(pointer, touches, primary, rx, ry):
358
+ """Follow one finger; re-anchor without a jump when it changes."""
359
+ live = {s: p for s, p in touches.items()
360
+ if p[0] is not None and p[1] is not None}
361
+ if not live:
362
+ return primary, rx, ry
363
+ cur = min(live)
364
+ x, y = live[cur]
365
+ if primary is None or primary[0] != cur:
366
+ return (cur, x, y), 0.0, 0.0
367
+ dx, dy = x - primary[1], y - primary[2]
368
+ if dx or dy:
369
+ speed = max(abs(dx), abs(dy))
370
+ scale = SENSITIVITY * min(ACCEL_MAX, 1.0 + speed / ACCEL_REF)
371
+ rx += dx * scale
372
+ ry += dy * scale
373
+ ix, iy = int(rx), int(ry)
374
+ rx -= ix
375
+ ry -= iy
376
+ if ix or iy:
377
+ if ix:
378
+ pointer.write(e.EV_REL, e.REL_X, ix)
379
+ if iy:
380
+ pointer.write(e.EV_REL, e.REL_Y, iy)
381
+ pointer.syn()
382
+ return (cur, x, y), rx, ry
383
+
384
+
385
+ def start_three_finger_drag():
386
+ """Idempotent module-level starter (safe across hotswap re-exec)."""
387
+ inst = globals().get("_instance")
388
+ if inst is not None and inst._thread is not None and inst._thread.is_alive():
389
+ return inst
390
+ inst = ThreeFingerDrag()
391
+ inst.start()
392
+ globals()["_instance"] = inst
393
+ return inst
@@ -0,0 +1,177 @@
1
+ """Render-function references and the caller's editable callee projection."""
2
+ import inspect
3
+ import sys
4
+
5
+ from meltygui.core.rendering.core_decoration import Core
6
+
7
+
8
+ def resolve_view_func(value):
9
+ """Resolve registered references without evaluating comment expressions."""
10
+ if value is None:
11
+ return None
12
+ if inspect.ismethod(value):
13
+ value = value.__func__
14
+ if callable(value):
15
+ name = getattr(value, "__name__", None)
16
+ registered = Core.melty.render_funcs_by_name.get(name)
17
+ if registered is not None and inspect.unwrap(registered) is inspect.unwrap(value):
18
+ return registered
19
+ from meltygui.core.rendering.render_funcs import _LazyRenderFunc
20
+ if isinstance(value, _LazyRenderFunc):
21
+ return value._resolve()
22
+ if hasattr(value, "__render_func__"):
23
+ return value
24
+ raise ValueError(f"Not a registered render function: {name or value!r}")
25
+ if isinstance(value, str):
26
+ reference = str(value)
27
+ prefix = "__import__('meltygui.core.rendering.render_funcs', fromlist=['RenderFuncs']).RenderFuncs."
28
+ if reference.startswith(prefix):
29
+ reference = reference[len(prefix):]
30
+ parts = reference.split(".")
31
+ if all(part.isidentifier() for part in parts):
32
+ candidate = Core.melty.render_funcs_by_name.get(parts[-1])
33
+ if candidate is not None:
34
+ module = getattr(inspect.unwrap(candidate), "__module__", "")
35
+ if len(parts) == 1 or ".".join(parts[:-1]) in ("RenderFuncs", "meltygui", module):
36
+ return candidate
37
+ raise ValueError(f"Unknown render function: {value}")
38
+ raise ValueError(f"Invalid render function reference: {value!r}")
39
+
40
+
41
+ def view_func_name(value):
42
+ return getattr(value, "__name__", str(value)) if value is not None else "Default"
43
+
44
+
45
+ def view_reference_code(value, filename=None):
46
+ """Use an existing binding, else an import expression valid in any module."""
47
+ from meltygui.code.libcst_conversion import CodeLine
48
+ function = resolve_view_func(value)
49
+ if function is None:
50
+ return None
51
+ from meltygui.core.rendering.render_funcs import RenderFuncs
52
+ for module in tuple(sys.modules.values()):
53
+ if module is None or not filename or getattr(module, "__file__", None) != str(filename):
54
+ continue
55
+ for name, binding in tuple(vars(module).items()):
56
+ if not name.isidentifier():
57
+ continue
58
+ if binding is function or binding is inspect.unwrap(function):
59
+ return CodeLine(name)
60
+ for name, binding in tuple(vars(module).items()):
61
+ if name.isidentifier() and binding is RenderFuncs:
62
+ return CodeLine(f"{name}.{function.__name__}")
63
+ return CodeLine("__import__('meltygui.core.rendering.render_funcs', "
64
+ f"fromlist=['RenderFuncs']).RenderFuncs.{function.__name__}")
65
+
66
+
67
+ def comment_view_func(input_value, kwargs):
68
+ """Own comment wins over the enclosing slot, just like the kwargs splat."""
69
+ selected = None
70
+ collection = kwargs.get("collection")
71
+ if isinstance(collection, dict) and "key" in kwargs:
72
+ overrides = collection.get("__overrides__", {})
73
+ if isinstance(overrides, dict):
74
+ entry = overrides.get(f"__{kwargs['key']}__", {})
75
+ if isinstance(entry, dict):
76
+ selected = entry.get("view_func")
77
+ if isinstance(input_value, dict):
78
+ overrides = input_value.get("__overrides__", {})
79
+ if isinstance(overrides, dict):
80
+ selected = overrides.get("view_func", selected)
81
+ return selected
82
+
83
+
84
+ def configured_view_func(input_value, kwargs, decoration=None, state_view=None):
85
+ """Resolve selection before entering a wrapper's layout/cache scopes."""
86
+ defaults = Core.melty.default_kwargs_by_type[kwargs.get("real_type", type(input_value))]
87
+ from meltygui.core.core_render import _codec_render_kwargs
88
+ selected = _codec_render_kwargs(type(input_value)).get("view_func")
89
+ if not isinstance(input_value, (dict, type)):
90
+ selected = getattr(input_value, "view_func", selected)
91
+ selected = (decoration or {}).get("view_func", selected)
92
+ selected = defaults.get("view_func", selected)
93
+ if isinstance(input_value, dict):
94
+ decorators = input_value.get("decorators", {})
95
+ parsed_defaults = decorators.get("defaults", {}) if isinstance(decorators, dict) else {}
96
+ if isinstance(parsed_defaults, dict):
97
+ selected = parsed_defaults.get("view_func", selected)
98
+ collection = kwargs.get("collection")
99
+ attributes = Core.melty.default_kwargs_by_attrib_type[type(collection)]
100
+ selected = attributes.get(kwargs.get("key"), {}).get("view_func", selected)
101
+ if state_view is not None:
102
+ selected = state_view
103
+ selected = kwargs.get("view_func") or selected
104
+ modes = kwargs.get("mode")
105
+ for mode in modes if isinstance(modes, tuple) else (modes,):
106
+ if mode is not None:
107
+ config = mode.get_config_for(input_value)
108
+ if config is not None and config.kwargs:
109
+ selected = config.kwargs.get("view_func", selected)
110
+ selected = comment_view_func(input_value, kwargs) or selected
111
+ return resolve_view_func(selected)
112
+
113
+
114
+ class CallerViewSource(dict):
115
+ """The call's arguments plus a virtual view_func backed by its callee."""
116
+ def __init__(self, parsed, filename=None, allow_direct=True):
117
+ super().__init__(parsed)
118
+ self.parsed = parsed
119
+ self.filename = filename
120
+ self.direct = False
121
+ if allow_direct and "view_func" not in parsed:
122
+ try:
123
+ function = resolve_view_func(parsed.get("__callee__", getattr(parsed, "func_name", None)))
124
+ except ValueError:
125
+ function = None
126
+ if function is not None:
127
+ self.direct = True
128
+ dict.__setitem__(self, "view_func", function)
129
+
130
+ def __setitem__(self, key, value):
131
+ if key == "view_func" and self.direct:
132
+ if value is None:
133
+ raise ValueError("A direct call needs a render function")
134
+ self.parsed["__callee__"] = view_reference_code(value, self.filename)
135
+ from meltygui.core.conversion.bubbling import _notify
136
+ _notify(self.parsed)
137
+ else:
138
+ self.parsed[key] = value
139
+ super().__setitem__(key, value)
140
+
141
+ def __delitem__(self, key):
142
+ self.pop(key)
143
+
144
+ def pop(self, key, default=None):
145
+ if key == "view_func" and self.direct:
146
+ raise ValueError("A direct call needs a render function")
147
+ self.parsed.pop(key, None)
148
+ return super().pop(key, default)
149
+
150
+
151
+ class WindowViewSource(CallerViewSource):
152
+ """Window decorator kwargs, with the decorated renderer as their default.
153
+
154
+ The implicit view_func exists only in this projection. Parameter edits
155
+ write through to the decorator parse; choosing another renderer creates
156
+ an explicit view_func kwarg through the same path.
157
+ """
158
+ def __init__(self, parsed, function, filename=None):
159
+ super().__init__(parsed, filename, allow_direct=False)
160
+ self.function = function
161
+ self.implicit_view_func = self.get("view_func") is None
162
+ if self.implicit_view_func:
163
+ dict.__setitem__(self, "view_func", function)
164
+
165
+ def __setitem__(self, key, value):
166
+ super().__setitem__(key, value)
167
+ if key == "view_func":
168
+ self.implicit_view_func = value is None
169
+ if self.implicit_view_func:
170
+ dict.__setitem__(self, key, self.function)
171
+
172
+ def pop(self, key, default=None):
173
+ value = super().pop(key, default)
174
+ if key == "view_func":
175
+ self.implicit_view_func = True
176
+ dict.__setitem__(self, key, self.function)
177
+ return value
@@ -0,0 +1 @@
1
+ """Shared framework layout machinery."""