gitpane 1.0.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 (353) hide show
  1. gitpane/__init__.py +0 -0
  2. gitpane/account/__init__.py +0 -0
  3. gitpane/account/account_choice.py +297 -0
  4. gitpane/account/address_owner.py +67 -0
  5. gitpane/account/authentication.py +1811 -0
  6. gitpane/account/authentication_worker.py +836 -0
  7. gitpane/account/credential_manager.py +480 -0
  8. gitpane/account/credential_manager_install.py +171 -0
  9. gitpane/account/keyring_reader.py +135 -0
  10. gitpane/account/keyring_state.py +158 -0
  11. gitpane/account/ssh.py +312 -0
  12. gitpane/account/ssh_askpass.py +216 -0
  13. gitpane/account/ssh_prompt.py +222 -0
  14. gitpane/application.py +684 -0
  15. gitpane/commit/__init__.py +0 -0
  16. gitpane/commit/commit_metadata.py +88 -0
  17. gitpane/commit/commit_push.py +276 -0
  18. gitpane/commit/commit_write.py +830 -0
  19. gitpane/config.py +1066 -0
  20. gitpane/controller/__init__.py +0 -0
  21. gitpane/controller/account_controller.py +677 -0
  22. gitpane/controller/clone_controller.py +776 -0
  23. gitpane/controller/close_readiness.py +212 -0
  24. gitpane/controller/commit_timing_controller.py +566 -0
  25. gitpane/controller/conflict_controller.py +885 -0
  26. gitpane/controller/console_registry.py +38 -0
  27. gitpane/controller/emoji_window_controller.py +95 -0
  28. gitpane/controller/file_action_controller.py +479 -0
  29. gitpane/controller/history_read_controller.py +671 -0
  30. gitpane/controller/history_rewrite_controller.py +739 -0
  31. gitpane/controller/navigation_controller.py +658 -0
  32. gitpane/controller/opening_controller.py +375 -0
  33. gitpane/controller/operation_policy.py +76 -0
  34. gitpane/controller/rebase_controller.py +797 -0
  35. gitpane/controller/rebase_editor_controller.py +309 -0
  36. gitpane/controller/reference_form_controller.py +1677 -0
  37. gitpane/controller/reference_read_controller.py +242 -0
  38. gitpane/controller/refresh_controller.py +477 -0
  39. gitpane/controller/replay_controller.py +805 -0
  40. gitpane/controller/session_registry.py +337 -0
  41. gitpane/controller/setting_controller.py +592 -0
  42. gitpane/controller/shutdown_controller.py +345 -0
  43. gitpane/controller/staging_controller.py +199 -0
  44. gitpane/controller/staging_session_registry.py +47 -0
  45. gitpane/controller/stash_controller.py +367 -0
  46. gitpane/controller/transport_controller.py +813 -0
  47. gitpane/controller/welcome_controller.py +102 -0
  48. gitpane/controller/window_activation.py +116 -0
  49. gitpane/controller/worktree_state_controller.py +169 -0
  50. gitpane/emoji/__init__.py +0 -0
  51. gitpane/emoji/emoji_catalogue.py +156 -0
  52. gitpane/git_config/__init__.py +0 -0
  53. gitpane/git_config/git_setting.py +441 -0
  54. gitpane/history/__init__.py +0 -0
  55. gitpane/history/avatar.py +241 -0
  56. gitpane/history/file_history.py +359 -0
  57. gitpane/history/graph.py +145 -0
  58. gitpane/history/history_filter.py +31 -0
  59. gitpane/history/history_parse.py +605 -0
  60. gitpane/history/repository.py +1600 -0
  61. gitpane/history/repository_value.py +341 -0
  62. gitpane/history/revision.py +180 -0
  63. gitpane/history/revision_file.py +182 -0
  64. gitpane/history_rewrite/__init__.py +0 -0
  65. gitpane/history_rewrite/graph.py +210 -0
  66. gitpane/history_rewrite/guard.py +288 -0
  67. gitpane/history_rewrite/index.py +125 -0
  68. gitpane/history_rewrite/prepare.py +378 -0
  69. gitpane/history_rewrite/protocol.py +320 -0
  70. gitpane/history_rewrite/publish.py +444 -0
  71. gitpane/history_rewrite/recovery.py +481 -0
  72. gitpane/history_rewrite/value.py +229 -0
  73. gitpane/operation/__init__.py +0 -0
  74. gitpane/operation/console_line.py +313 -0
  75. gitpane/operation/console_tone.py +76 -0
  76. gitpane/operation/diagnostic.py +954 -0
  77. gitpane/operation/launch_signature.py +254 -0
  78. gitpane/operation/operation_status.py +87 -0
  79. gitpane/operation/operation_trace.py +766 -0
  80. gitpane/operation/repository_operation.py +781 -0
  81. gitpane/preference/__init__.py +0 -0
  82. gitpane/preference/preference_location.py +40 -0
  83. gitpane/preference/preference_store.py +590 -0
  84. gitpane/process/__init__.py +0 -0
  85. gitpane/process/exact_arguments.py +77 -0
  86. gitpane/process/git_output_text.py +20 -0
  87. gitpane/process/process_group.py +851 -0
  88. gitpane/process/process_log.py +114 -0
  89. gitpane/process/process_runner.py +1033 -0
  90. gitpane/process/released_child_watch.py +138 -0
  91. gitpane/recent/__init__.py +0 -0
  92. gitpane/recent/mount_table.py +422 -0
  93. gitpane/recent/recent_availability.py +414 -0
  94. gitpane/recent/recent_name.py +11 -0
  95. gitpane/recent/recent_search.py +114 -0
  96. gitpane/reference/__init__.py +0 -0
  97. gitpane/reference/reference_edit.py +2030 -0
  98. gitpane/reference/reference_snapshot.py +196 -0
  99. gitpane/reference/stash.py +730 -0
  100. gitpane/reference/stash_list.py +164 -0
  101. gitpane/sequencer/__init__.py +0 -0
  102. gitpane/sequencer/commit_message_rule.py +154 -0
  103. gitpane/sequencer/conflict.py +506 -0
  104. gitpane/sequencer/merge_recovery.py +163 -0
  105. gitpane/sequencer/merge_state.py +166 -0
  106. gitpane/sequencer/mergetool.py +856 -0
  107. gitpane/sequencer/rebase.py +750 -0
  108. gitpane/sequencer/rebase_edit_file.py +224 -0
  109. gitpane/sequencer/rebase_state.py +523 -0
  110. gitpane/sequencer/replay.py +1435 -0
  111. gitpane/sequencer/replay_legacy.py +253 -0
  112. gitpane/sequencer/replay_state.py +747 -0
  113. gitpane/submodule/__init__.py +0 -0
  114. gitpane/submodule/checkout_refusal.py +102 -0
  115. gitpane/submodule/checkout_refusal_notice.py +111 -0
  116. gitpane/submodule/cloned_without_checkout.py +65 -0
  117. gitpane/submodule/submodule_discovery.py +1180 -0
  118. gitpane/submodule/submodule_loss.py +218 -0
  119. gitpane/system/__init__.py +0 -0
  120. gitpane/system/appearance_portal.py +125 -0
  121. gitpane/system/application_identity.py +48 -0
  122. gitpane/system/command_line.py +36 -0
  123. gitpane/system/desktop_icon_fact.py +128 -0
  124. gitpane/system/desktop_icon_support.py +270 -0
  125. gitpane/system/desktop_shortcut.py +536 -0
  126. gitpane/system/icns_image.py +35 -0
  127. gitpane/system/installed_launcher.py +136 -0
  128. gitpane/system/linux_distribution.py +151 -0
  129. gitpane/system/network_folder.py +46 -0
  130. gitpane/system/path_identity.py +95 -0
  131. gitpane/system/private_tree_removal.py +47 -0
  132. gitpane/system/process_program.py +63 -0
  133. gitpane/system/qt_folder.py +38 -0
  134. gitpane/system/ready_channel_watch.py +67 -0
  135. gitpane/system/startup_activation.py +65 -0
  136. gitpane/system/terminal_detach.py +384 -0
  137. gitpane/system/xdg_session.py +85 -0
  138. gitpane/text/__init__.py +0 -0
  139. gitpane/text/text_query.py +97 -0
  140. gitpane/text/typed_path.py +75 -0
  141. gitpane/text/typed_text.py +17 -0
  142. gitpane/thread/__init__.py +0 -0
  143. gitpane/thread/weak_signal.py +93 -0
  144. gitpane/tool/__init__.py +0 -0
  145. gitpane/tool/app_bundle.py +164 -0
  146. gitpane/tool/desktop_text_editor.py +156 -0
  147. gitpane/tool/difftool.py +429 -0
  148. gitpane/tool/editor.py +445 -0
  149. gitpane/tool/editor_bridge.py +193 -0
  150. gitpane/tool/editor_recipe.py +481 -0
  151. gitpane/tool/file_reveal.py +94 -0
  152. gitpane/tool/git_executable.py +264 -0
  153. gitpane/tool/git_version.py +62 -0
  154. gitpane/tool/kdiff3.py +41 -0
  155. gitpane/tool/kdiff3_install.py +162 -0
  156. gitpane/tool/launch_services_text_editor.py +104 -0
  157. gitpane/tool/mergetool_recipe.py +84 -0
  158. gitpane/tool/system_editor_default.py +80 -0
  159. gitpane/tool/system_text_editor.py +44 -0
  160. gitpane/tool/system_tool.py +816 -0
  161. gitpane/tool/terminal_recipe.py +390 -0
  162. gitpane/transport/__init__.py +0 -0
  163. gitpane/transport/branch_exchange.py +1690 -0
  164. gitpane/transport/clone.py +1273 -0
  165. gitpane/transport/credential_read_retry.py +35 -0
  166. gitpane/transport/git_refusal.py +207 -0
  167. gitpane/transport/push_tracking.py +121 -0
  168. gitpane/transport/refusal_notice.py +225 -0
  169. gitpane/transport/remote.py +747 -0
  170. gitpane/transport/remote_config.py +544 -0
  171. gitpane/ui/__init__.py +0 -0
  172. gitpane/ui/console/__init__.py +0 -0
  173. gitpane/ui/console/error_dialog.py +332 -0
  174. gitpane/ui/console/operation_dialog.py +408 -0
  175. gitpane/ui/console/operation_output.py +91 -0
  176. gitpane/ui/console/operation_output_view.py +373 -0
  177. gitpane/ui/emoji/__init__.py +0 -0
  178. gitpane/ui/emoji/emoji_font.py +257 -0
  179. gitpane/ui/emoji/emoji_grid.py +125 -0
  180. gitpane/ui/emoji/emoji_window.py +154 -0
  181. gitpane/ui/emoji/file_action_icon.py +131 -0
  182. gitpane/ui/emoji/opentype_family_name.py +182 -0
  183. gitpane/ui/history_rewrite/__init__.py +0 -0
  184. gitpane/ui/history_rewrite/details.py +78 -0
  185. gitpane/ui/history_rewrite/preview.py +565 -0
  186. gitpane/ui/history_rewrite/preview_model.py +291 -0
  187. gitpane/ui/history_rewrite/question.py +87 -0
  188. gitpane/ui/history_rewrite/recovery_dialog.py +262 -0
  189. gitpane/ui/look/__init__.py +0 -0
  190. gitpane/ui/look/action_icon.py +504 -0
  191. gitpane/ui/look/application_icon.py +78 -0
  192. gitpane/ui/look/application_style.py +317 -0
  193. gitpane/ui/look/avatar_label.py +53 -0
  194. gitpane/ui/look/color_contrast.py +43 -0
  195. gitpane/ui/look/desktop_appearance_follower.py +118 -0
  196. gitpane/ui/look/fixed_font.py +21 -0
  197. gitpane/ui/look/gtk_settings.py +116 -0
  198. gitpane/ui/look/icon_density.py +16 -0
  199. gitpane/ui/look/reference_font.py +19 -0
  200. gitpane/ui/look/theme_color.py +63 -0
  201. gitpane/ui/look/theme_look.py +95 -0
  202. gitpane/ui/look/theme_palette.py +173 -0
  203. gitpane/ui/look/warning_icon.py +86 -0
  204. gitpane/ui/look/windows_control_look.py +250 -0
  205. gitpane/ui/reader/__init__.py +0 -0
  206. gitpane/ui/reader/commit_message.py +129 -0
  207. gitpane/ui/reader/commit_panel.py +378 -0
  208. gitpane/ui/reader/diff_panel.py +626 -0
  209. gitpane/ui/reader/file_action.py +138 -0
  210. gitpane/ui/reader/file_tree_panel.py +235 -0
  211. gitpane/ui/reader/reader_toolbar.py +56 -0
  212. gitpane/ui/reader/revision_tree.py +80 -0
  213. gitpane/ui/reader/search_result.py +165 -0
  214. gitpane/ui/reader/source_text.py +134 -0
  215. gitpane/ui/reader/source_view.py +151 -0
  216. gitpane/ui/reader/syntax_span.py +67 -0
  217. gitpane/ui/reference/__init__.py +0 -0
  218. gitpane/ui/reference/branch_selector.py +106 -0
  219. gitpane/ui/reference/checkout_dialog.py +419 -0
  220. gitpane/ui/reference/checkout_revision_dialog.py +182 -0
  221. gitpane/ui/reference/commit_summary.py +339 -0
  222. gitpane/ui/reference/create_branch_dialog.py +346 -0
  223. gitpane/ui/reference/create_tag_dialog.py +283 -0
  224. gitpane/ui/reference/delete_local_branch_dialog.py +323 -0
  225. gitpane/ui/reference/delete_tag_dialog.py +394 -0
  226. gitpane/ui/reference/form_opening.py +53 -0
  227. gitpane/ui/reference/reference_dialog.py +417 -0
  228. gitpane/ui/reference/reference_feedback.py +62 -0
  229. gitpane/ui/reference/reference_form.py +139 -0
  230. gitpane/ui/reference/rename_branch_dialog.py +179 -0
  231. gitpane/ui/reference/reset_branch_dialog.py +430 -0
  232. gitpane/ui/reference/reset_dialog.py +263 -0
  233. gitpane/ui/reference/revision_dialog.py +121 -0
  234. gitpane/ui/reference/revision_picker.py +227 -0
  235. gitpane/ui/reference/select_branches_dialog.py +199 -0
  236. gitpane/ui/reference/stash_dialog.py +171 -0
  237. gitpane/ui/repository/__init__.py +0 -0
  238. gitpane/ui/repository/graph_delegate.py +298 -0
  239. gitpane/ui/repository/history_avatar.py +130 -0
  240. gitpane/ui/repository/history_model.py +179 -0
  241. gitpane/ui/repository/repository_list_popup.py +319 -0
  242. gitpane/ui/repository/repository_window.py +2580 -0
  243. gitpane/ui/resource/ArrowDown.png +0 -0
  244. gitpane/ui/resource/ArrowUp.png +0 -0
  245. gitpane/ui/resource/BranchCheckout.png +0 -0
  246. gitpane/ui/resource/BranchCreate.png +0 -0
  247. gitpane/ui/resource/BranchDelete.png +0 -0
  248. gitpane/ui/resource/CopyToClipboard.png +0 -0
  249. gitpane/ui/resource/GitPane-16.png +0 -0
  250. gitpane/ui/resource/GitPane-24.png +0 -0
  251. gitpane/ui/resource/GitPane-256.png +0 -0
  252. gitpane/ui/resource/GitPane-32.png +0 -0
  253. gitpane/ui/resource/GitPane-48.png +0 -0
  254. gitpane/ui/resource/GitPane-64.png +0 -0
  255. gitpane/ui/resource/GitPane.ico +0 -0
  256. gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
  257. gitpane/ui/resource/NotoColorEmoji_WindowsCompatible.ttf +0 -0
  258. gitpane/ui/resource/Renamed.png +0 -0
  259. gitpane/ui/resource/ResetAnotherBranchToHere.png +0 -0
  260. gitpane/ui/resource/ResetCurrentBranchToHere.png +0 -0
  261. gitpane/ui/resource/Stash.png +0 -0
  262. gitpane/ui/resource/Tag.png +0 -0
  263. gitpane/ui/resource/TagCreate.png +0 -0
  264. gitpane/ui/resource/TagDelete.png +0 -0
  265. gitpane/ui/resource/Unmerged.png +0 -0
  266. gitpane/ui/resource/checkout.png +0 -0
  267. gitpane/ui/sequencer/__init__.py +0 -0
  268. gitpane/ui/sequencer/conflict_dialog.py +532 -0
  269. gitpane/ui/sequencer/merge_recovery_dialog.py +67 -0
  270. gitpane/ui/sequencer/rebase_dialog.py +461 -0
  271. gitpane/ui/sequencer/rebase_editor_window.py +220 -0
  272. gitpane/ui/sequencer/replay_conflict_prompt.py +105 -0
  273. gitpane/ui/sequencer/replay_dialog.py +501 -0
  274. gitpane/ui/setting/__init__.py +0 -0
  275. gitpane/ui/setting/account_page.py +724 -0
  276. gitpane/ui/setting/add_account_dialog.py +1005 -0
  277. gitpane/ui/setting/credential_manager_dialog.py +151 -0
  278. gitpane/ui/setting/git_setting_widget.py +388 -0
  279. gitpane/ui/setting/setting_block.py +19 -0
  280. gitpane/ui/setting/setting_dialog.py +750 -0
  281. gitpane/ui/setting/ssh_widget.py +232 -0
  282. gitpane/ui/setting/utility_page.py +240 -0
  283. gitpane/ui/staging/__init__.py +0 -0
  284. gitpane/ui/staging/staging_action.py +356 -0
  285. gitpane/ui/staging/staging_window.py +745 -0
  286. gitpane/ui/transport/__init__.py +0 -0
  287. gitpane/ui/transport/clone_dialog.py +879 -0
  288. gitpane/ui/transport/push_dialog.py +580 -0
  289. gitpane/ui/transport/push_rejection_prompt.py +100 -0
  290. gitpane/ui/transport/push_tracking_question.py +88 -0
  291. gitpane/ui/transport/remote_dialog.py +943 -0
  292. gitpane/ui/welcome/__init__.py +0 -0
  293. gitpane/ui/welcome/invalid_repository_prompt.py +114 -0
  294. gitpane/ui/welcome/welcome_window.py +1137 -0
  295. gitpane/ui/widget/__init__.py +0 -0
  296. gitpane/ui/widget/busy_indicator.py +106 -0
  297. gitpane/ui/widget/commit_date_text.py +55 -0
  298. gitpane/ui/widget/content_growth.py +262 -0
  299. gitpane/ui/widget/content_measure.py +99 -0
  300. gitpane/ui/widget/dialog_layout.py +668 -0
  301. gitpane/ui/widget/drop_down_wheel_filter.py +32 -0
  302. gitpane/ui/widget/flow_layout.py +110 -0
  303. gitpane/ui/widget/long_text.py +542 -0
  304. gitpane/ui/widget/mnemonic.py +13 -0
  305. gitpane/ui/widget/numbered_text_edit.py +127 -0
  306. gitpane/ui/widget/plain_label.py +15 -0
  307. gitpane/ui/widget/plain_tooltip.py +19 -0
  308. gitpane/ui/widget/refresh_advice.py +25 -0
  309. gitpane/ui/widget/result_label.py +227 -0
  310. gitpane/ui/widget/result_status_bar.py +53 -0
  311. gitpane/ui/widget/scrolled_text.py +69 -0
  312. gitpane/ui/widget/shortcut_text.py +31 -0
  313. gitpane/ui/widget/standard_icon_label.py +55 -0
  314. gitpane/ui/widget/system_shortcut.py +75 -0
  315. gitpane/ui/widget/unavailable_reason.py +29 -0
  316. gitpane/ui/widget/wheel_safe_drop_down.py +24 -0
  317. gitpane/ui/window_control/__init__.py +0 -0
  318. gitpane/ui/window_control/button_focus.py +42 -0
  319. gitpane/ui/window_control/compositor_answer.py +152 -0
  320. gitpane/ui/window_control/compositor_growth_cap.py +73 -0
  321. gitpane/ui/window_control/desktop_activation.py +203 -0
  322. gitpane/ui/window_control/dialog_modality.py +122 -0
  323. gitpane/ui/window_control/item_activation.py +42 -0
  324. gitpane/ui/window_control/modal_focus_relay.py +124 -0
  325. gitpane/ui/window_control/wayland_activation.py +695 -0
  326. gitpane/ui/window_control/window_placement.py +305 -0
  327. gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
  328. gitpane/vendor/w32.exe +0 -0
  329. gitpane/vendor/w64-arm.exe +0 -0
  330. gitpane/vendor/w64.exe +0 -0
  331. gitpane/worktree/__init__.py +0 -0
  332. gitpane/worktree/command_sequence.py +110 -0
  333. gitpane/worktree/file_action_target.py +61 -0
  334. gitpane/worktree/file_menu_action.py +52 -0
  335. gitpane/worktree/historical_restore.py +361 -0
  336. gitpane/worktree/ignore_rule.py +266 -0
  337. gitpane/worktree/mutation.py +1539 -0
  338. gitpane/worktree/partial_patch.py +435 -0
  339. gitpane/worktree/staged_reset.py +447 -0
  340. gitpane/worktree/staging.py +1418 -0
  341. gitpane/worktree/trash.py +302 -0
  342. gitpane/worktree/worktree_file.py +406 -0
  343. gitpane/worktree/worktree_service.py +584 -0
  344. gitpane/worktree/worktree_snapshot.py +630 -0
  345. gitpane-1.0.0.dist-info/METADATA +141 -0
  346. gitpane-1.0.0.dist-info/RECORD +353 -0
  347. gitpane-1.0.0.dist-info/WHEEL +5 -0
  348. gitpane-1.0.0.dist-info/entry_points.txt +2 -0
  349. gitpane-1.0.0.dist-info/licenses/LICENSE +21 -0
  350. gitpane-1.0.0.dist-info/licenses/THIRD_PARTY_NOTICES.md +109 -0
  351. gitpane-1.0.0.dist-info/licenses/src/gitpane/ui/resource/NotoColorEmoji-OFL.txt +92 -0
  352. gitpane-1.0.0.dist-info/licenses/src/gitpane/vendor/DISTLIB-LICENSE.txt +284 -0
  353. gitpane-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,75 @@
1
+ """Turn a path the user typed into the path GitPane uses, expanding ~ to the home folder where that is the usage."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ # Only Linux and macOS expand ~, and both write their paths the POSIX way: posixpath gives the same answer when the
8
+ # tests of these two systems run on Windows, where os.path would be ntpath.
9
+ import posixpath
10
+ import sys
11
+ from typing import TYPE_CHECKING
12
+
13
+ from gitpane.config import LINUX_PLATFORM, MACOS_PLATFORM
14
+ from gitpane.text.typed_text import typed_value
15
+
16
+ if TYPE_CHECKING:
17
+ from collections.abc import Callable
18
+
19
+ HOME_SHORTHAND = "~"
20
+ HOME_SHORTHAND_SEPARATOR = "/"
21
+ HOME_SHORTHAND_PLATFORM_SET = frozenset({LINUX_PLATFORM, MACOS_PLATFORM})
22
+
23
+
24
+ def expanded_typed_path(path: str, platform: str, home_dir: str) -> str:
25
+ """Return the path to use for ``path`` as typed on ``platform``, ``~`` alone or ``~/`` at its start standing for ``home_dir``.
26
+
27
+ A shell on Linux and macOS reads ``~`` as the home folder, so a path copied from a terminal works as typed. Windows
28
+ keeps the text as typed: ``~`` is not the usage there, and ``~`` is a valid character of a folder name. ``~name``,
29
+ the home folder of another user, stays as typed: expanding it would mean reading the system's user database. A
30
+ ``~`` anywhere else than at the start, an unknown system, and a home folder that is empty or not absolute leave the
31
+ path as typed too, so nothing is ever guessed. ``~``, ``~/`` and ``~//`` give the home folder as it is, without a
32
+ trailing separator, so a Clone destination typed ``~/`` is saved as the same folder typed without the ``/``.
33
+ """
34
+ if platform not in HOME_SHORTHAND_PLATFORM_SET:
35
+ return path
36
+ if not home_dir or not posixpath.isabs(home_dir):
37
+ return path
38
+ if path == HOME_SHORTHAND:
39
+ return home_dir
40
+ if not path.startswith(HOME_SHORTHAND + HOME_SHORTHAND_SEPARATOR):
41
+ return path
42
+ rest = path[len(HOME_SHORTHAND):].lstrip(HOME_SHORTHAND_SEPARATOR)
43
+ if not rest:
44
+ return home_dir
45
+ return posixpath.join(home_dir, rest)
46
+
47
+
48
+ def session_typed_path(path: str) -> str:
49
+ """Return the path to use for ``path`` as typed in this session, on this system and with this user's home folder."""
50
+ return expanded_typed_path(path, sys.platform, os.path.expanduser(HOME_SHORTHAND))
51
+
52
+
53
+ def typed_path_value(text: str, typed_path: Callable[[str], str]) -> str:
54
+ """Return the value a path field hands to its action: ``text`` without the blanks around it, then read by ``typed_path``.
55
+
56
+ The blanks go first, so that a ``~/`` path pasted with a blank in front still stands for the home folder. Every path
57
+ field reads its text through here, so the blanks and ``~`` are read in one place for all of them. ``typed_path`` is
58
+ asked for, never taken by default: each window hands its injected reading, which the tests replace with their own
59
+ system and home folder. Nothing is read from the disk.
60
+ """
61
+ return typed_path(typed_value(text))
62
+
63
+
64
+ def typed_start_dir(text: str, typed_path: Callable[[str], str]) -> str:
65
+ """Return the folder a chooser next to a path field starts from: the path the field stands for, when it is absolute.
66
+
67
+ The field's value is read like its action reads it, blanks dropped and ``~`` read where that is the usage, so the
68
+ chooser opens where the typed path points. A value that is not absolute on this system gives an empty folder, and
69
+ the chooser then starts from its own folder, or from the fallback the window gives; a remote address is never
70
+ absolute, so it never names a start folder. Nothing is read from the disk.
71
+ """
72
+ value = typed_path_value(text, typed_path)
73
+ if not os.path.isabs(value):
74
+ return ""
75
+ return value
@@ -0,0 +1,17 @@
1
+ """What GitPane uses of a text the user typed: the text without the blanks around it."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ def typed_value(text: str) -> str:
7
+ """Return the typed text without the blanks around it, its inside untouched.
8
+
9
+ Spaces, tabs, line ends and no-break spaces go from both ends; inner spaces, lines and blank lines stay exactly as
10
+ typed, and a text made only of blanks becomes empty, so it counts as an empty field. A name, a path or an address
11
+ pasted with a space around it would otherwise be refused by Git or open nothing, while a message keeps its inner
12
+ lines. The field itself is never rewritten: only the value GitPane uses loses its blanks.
13
+
14
+ Not for secrets typed as they are (passwords, tokens, answers to an SSH question), where a space may be part of the
15
+ secret, nor for search fields, where spaces are part of what is searched.
16
+ """
17
+ return text.strip()
File without changes
@@ -0,0 +1,93 @@
1
+ """Hand something over from a background thread to the thread of a Qt object, without keeping that object alive."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ import weakref
7
+ from typing import TYPE_CHECKING
8
+
9
+ from PySide6.QtCore import QCoreApplication, QObject, Signal, Slot
10
+ from shiboken6 import isValid
11
+
12
+ if TYPE_CHECKING:
13
+ from collections.abc import Callable
14
+
15
+ from PySide6.QtCore import SignalInstance
16
+
17
+
18
+ class SignalRelay(QObject):
19
+ """A Qt object of the window's thread that background threads signal in place of the window, which then signals the window on that thread.
20
+
21
+ A window can be destroyed on its own thread at any moment, for example the user closing a form while a check still
22
+ runs. A worker that emitted the window's own signal could do so while Qt is destroying it on the other thread, which
23
+ PySide then refuses with an error raised on the worker, or worse, since touching a Qt object under destruction from
24
+ another thread is undefined in C++. The worker only ever emits this relay's signal; Qt queues it to the relay's
25
+ thread, the window's own, where the relay finds out whether the window still exists before emitting its signal.
26
+ """
27
+
28
+ handed_over = Signal(object, object, object)
29
+
30
+ def __init__(self) -> None:
31
+ super().__init__()
32
+ self.handed_over.connect(self.deliver)
33
+
34
+ @Slot(object, object, object)
35
+ def deliver(self, owner_reference: weakref.ref[QObject], signal_of: Callable[[QObject], SignalInstance], arguments: tuple[object, ...]) -> None:
36
+ """On the owner's thread, emit the arguments through the owner's signal, or drop them when the owner is gone or being destroyed."""
37
+ owner = owner_reference()
38
+ if owner is None or not isValid(owner):
39
+ return
40
+ signal_of(owner).emit(*arguments)
41
+
42
+
43
+ @functools.cache
44
+ def window_thread_relay() -> SignalRelay:
45
+ """Return the one relay of the window thread, the application's thread, whichever thread asks for it first.
46
+
47
+ One relay serves every window, so opening and closing windows leaves no relay behind. A Qt object belongs to the
48
+ thread that makes it, so a relay made by a first call on a worker is moved to the application's thread: cached on
49
+ the worker's thread, it would refuse every window for the rest of the process. Before any application exists there
50
+ is no such thread, and the relay stays on the thread that made it. It has no Qt parent and this cache keeps it until
51
+ the interpreter ends: it is never deleted while a worker may still signal it, and never from a worker thread, which
52
+ dropping the last Python reference of a relay there would do.
53
+ """
54
+ relay = SignalRelay()
55
+ application = QCoreApplication.instance()
56
+ if application is not None and relay.thread() is not application.thread():
57
+ relay.moveToThread(application.thread())
58
+ return relay
59
+
60
+
61
+ def weak_signal_emitter[OwnerT: QObject](owner: OwnerT, signal_of: Callable[[OwnerT], SignalInstance]) -> Callable[..., None]:
62
+ """Return a function a background check calls on its threads to hand something over to the owner's own thread.
63
+
64
+ Called on the owner's thread, the window thread. The function emits the relay's signal, never the owner's: the
65
+ relay emits the owner's signal that ``signal_of`` picks on the owner's thread, where the owner connects it with a
66
+ queued connection. It holds the owner only weakly: the check, and a thread blocked on a sleeping disk, must not keep
67
+ a closed window's Python object alive, nor tie it to the check in a cycle only the garbage collector would break.
68
+ """
69
+ relay = window_thread_relay()
70
+ if owner.thread() is not relay.thread():
71
+ message = "weak_signal_emitter serves Qt objects of the window thread only"
72
+ raise ValueError(message)
73
+ owner_reference = weakref.ref(owner)
74
+
75
+ def emit(*arguments: object) -> None:
76
+ """Hand the arguments over to the relay, which emits them through the owner's signal if the owner still exists then."""
77
+ relay.handed_over.emit(owner_reference, signal_of, arguments)
78
+
79
+ return emit
80
+
81
+
82
+ def hand_over_to_the_page(hand_over: Callable[[object], None], answer: object) -> None:
83
+ """Hand ``answer`` over to the thread of the window or page that waits for it, saying nothing when it cannot be handed over.
84
+
85
+ A window closed while the worker ran no longer raises anything: the relay drops its answer on the window's thread.
86
+ The relay itself is only deleted by the clean-up PySide may run as the interpreter ends, while a daemon worker can
87
+ still answer; Qt then refuses to emit through the deleted relay with ``RuntimeError``, and that answer has no window
88
+ left to show it either.
89
+ """
90
+ try:
91
+ hand_over(answer)
92
+ except RuntimeError:
93
+ pass
File without changes
@@ -0,0 +1,164 @@
1
+ """Read macOS applications (``.app`` folders) with neither Qt nor any child process.
2
+
3
+ An application on macOS is a folder: its ``Contents/Info.plist`` names the program under ``Contents/MacOS`` and the
4
+ identifier the system knows the application by. GitPane reads it to know which applications are installed and which
5
+ program to start, for terminals and text editors. Reading a few small files costs no process, unlike asking Spotlight,
6
+ which may also be switched off.
7
+
8
+ Everything a bundle declares is data written by whoever made the bundle, so a declared program is only accepted when it
9
+ is a plain file name that really is an executable file inside the bundle.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import plistlib
16
+ import xml.parsers.expat
17
+ from dataclasses import dataclass
18
+
19
+ BUNDLE_SUFFIX = ".app"
20
+ CONTENTS_DIR_BASENAME = "Contents"
21
+ INFO_PLIST_BASENAME = "Info.plist"
22
+ MACOS_DIR_BASENAME = "MacOS"
23
+ # Keys of Info.plist: the name of the program under Contents/MacOS, and the identifier that names the application
24
+ # whatever its folder is called.
25
+ BUNDLE_EXECUTABLE_KEY = "CFBundleExecutable"
26
+ BUNDLE_IDENTIFIER_KEY = "CFBundleIdentifier"
27
+ # TextEdit returns as soon as the file is open, so it is recognised by its identifier, which a renamed folder keeps.
28
+ TEXTEDIT_BUNDLE_IDENTIFIER = "com.apple.TextEdit"
29
+ # Where applications are installed, searched in this order: the shared folder, the applications macOS ships, its
30
+ # utilities (Terminal), then the user's own folder, where JetBrains Toolbox puts its IDEs.
31
+ SHARED_APPLICATIONS_DIR = "/Applications"
32
+ SYSTEM_APPLICATIONS_DIR = "/System/Applications"
33
+ APPLICATION_SEARCH_DIRS = (
34
+ SHARED_APPLICATIONS_DIR,
35
+ SYSTEM_APPLICATIONS_DIR,
36
+ os.path.join(SYSTEM_APPLICATIONS_DIR, "Utilities"),
37
+ os.path.expanduser(os.path.join("~", "Applications")),
38
+ )
39
+ # What plistlib raises on a file it cannot read: a missing or unreadable file, a broken or truncated XML or binary plist,
40
+ # and an XML plist that declares an entity, which plistlib refuses on purpose.
41
+ PLIST_READ_ERRORS = (OSError, ValueError, plistlib.InvalidFileException, xml.parsers.expat.ExpatError)
42
+
43
+
44
+ @dataclass(frozen=True)
45
+ class AppBundle:
46
+ """An application folder with the program it starts and the identifier it declares (empty when it declares none)."""
47
+
48
+ bundle_dir: str
49
+ executable_filename: str
50
+ identifier: str
51
+
52
+
53
+ def _is_executable_file(filename: str) -> bool:
54
+ """Tell whether ``filename`` is a regular file this user may run; a folder never is."""
55
+ return os.path.isfile(filename) and os.access(filename, os.X_OK)
56
+
57
+
58
+ def _is_plain_name(name: object) -> bool:
59
+ """Tell whether ``name`` is a non-empty file name with no folder in it and is neither ``.`` nor ``..``."""
60
+ if not isinstance(name, str) or not name:
61
+ return False
62
+ if name in (os.curdir, os.pardir):
63
+ return False
64
+ for separator in (os.sep, os.altsep):
65
+ if separator and separator in name:
66
+ return False
67
+ return True
68
+
69
+
70
+ def read_bundle(bundle_dir: str) -> AppBundle | None:
71
+ """Read the program and identifier a ``.app`` folder declares, or None when it is not a readable application.
72
+
73
+ Both XML and binary Info.plist files are read. The declared program must be a plain name, since it comes from the
74
+ bundle, and must be an executable file under ``Contents/MacOS``; anything else gives None instead of raising, so a
75
+ broken or crafted application is simply not offered.
76
+ """
77
+ try:
78
+ with open(os.path.join(bundle_dir, CONTENTS_DIR_BASENAME, INFO_PLIST_BASENAME), "rb") as stream:
79
+ info_by_key = plistlib.load(stream)
80
+ except PLIST_READ_ERRORS:
81
+ return None
82
+ if not isinstance(info_by_key, dict):
83
+ return None
84
+ executable_basename = info_by_key.get(BUNDLE_EXECUTABLE_KEY)
85
+ if not _is_plain_name(executable_basename):
86
+ return None
87
+ executable_filename = os.path.join(bundle_dir, CONTENTS_DIR_BASENAME, MACOS_DIR_BASENAME, executable_basename)
88
+ if not _is_executable_file(executable_filename):
89
+ return None
90
+ identifier = info_by_key.get(BUNDLE_IDENTIFIER_KEY)
91
+ if not isinstance(identifier, str):
92
+ identifier = ""
93
+ return AppBundle(bundle_dir, executable_filename, identifier)
94
+
95
+
96
+ def bundled_program(bundle_dir: str, relative_parts: tuple[str, ...]) -> str:
97
+ """Return the program at ``relative_parts`` inside ``bundle_dir`` when it is an executable file there, else ``""``.
98
+
99
+ Some applications ship their command line tool at a known place other than the declared program (an editor's
100
+ ``code`` or ``subl``). Those places are written in GitPane's own tables, so a part that is not a plain name is a
101
+ programming error and raises ValueError, which also keeps the result inside the bundle.
102
+ """
103
+ for part in relative_parts:
104
+ if not _is_plain_name(part):
105
+ raise ValueError(f"Bundle path part {part!r} is not a plain file name")
106
+ program_filename = os.path.join(bundle_dir, *relative_parts)
107
+ if not _is_executable_file(program_filename):
108
+ return ""
109
+ return program_filename
110
+
111
+
112
+ def find_application(bundle_basenames: tuple[str, ...], search_dirs: tuple[str, ...] = APPLICATION_SEARCH_DIRS) -> AppBundle | None:
113
+ """Return the first readable application named in ``bundle_basenames``, searching each folder for every name before the next folder.
114
+
115
+ The names are known, so no folder is listed: the order comes from the arguments alone, never from the order the
116
+ file system returns entries in. A missing or unreadable folder, or a candidate that is not a readable application,
117
+ is passed over.
118
+ """
119
+ for search_dir in search_dirs:
120
+ for bundle_basename in bundle_basenames:
121
+ bundle_dir = os.path.join(search_dir, bundle_basename)
122
+ if not os.path.isdir(bundle_dir):
123
+ continue
124
+ bundle = read_bundle(bundle_dir)
125
+ if bundle is not None:
126
+ return bundle
127
+ return None
128
+
129
+
130
+ def enclosing_bundle_dir(filename: str) -> str:
131
+ """Return the innermost ``.app`` folder that holds ``filename`` (or is ``filename``), or ``""`` when there is none.
132
+
133
+ A program typed by hand may sit inside an application; climbing to that application lets GitPane judge the
134
+ application, for instance refuse TextEdit, and not only the program.
135
+ """
136
+ current_filename = filename
137
+ while current_filename:
138
+ if os.path.basename(current_filename).endswith(BUNDLE_SUFFIX):
139
+ return current_filename
140
+ parent_filename = os.path.dirname(current_filename)
141
+ if parent_filename == current_filename:
142
+ return ""
143
+ current_filename = parent_filename
144
+ return ""
145
+
146
+
147
+ def is_textedit(bundle: AppBundle) -> bool:
148
+ """Tell whether ``bundle`` is TextEdit, by its identifier, so a renamed folder neither hides it nor imitates it."""
149
+ return bundle.identifier == TEXTEDIT_BUNDLE_IDENTIFIER
150
+
151
+
152
+ def chosen_application_program(filename: str) -> str:
153
+ """Return the program a file picked in a file chooser stands for: a readable ``.app`` folder gives the program it declares.
154
+
155
+ The macOS file panel returns an application as its folder, which cannot be started as a command. Any other file,
156
+ and an application GitPane cannot read, is returned as picked, so the usual check of the field reports it.
157
+ """
158
+ bundle_dir = filename.rstrip(os.sep) or filename
159
+ if not bundle_dir.endswith(BUNDLE_SUFFIX):
160
+ return filename
161
+ bundle = read_bundle(bundle_dir)
162
+ if bundle is None:
163
+ return filename
164
+ return bundle.executable_filename
@@ -0,0 +1,156 @@
1
+ """Name the application the desktop opens for a plain text file, read from its mimeapps.list files only.
2
+
3
+ The files are read in the order of the freedesktop MIME Applications specification, and only their
4
+ ``[Default Applications]`` group counts: it holds what the user, the distribution or the desktop chose. The cache of
5
+ MIME types (``mimeinfo.cache``) is never read: it lists every application that can open a text file, a word processor
6
+ included, so its first entry is not a choice anybody made. The identifier found only picks one of GitPane's known
7
+ editor presets; nothing of the desktop file it names is read or started.
8
+
9
+ Every function but the last receives the environment, the home folder, the system, the Flatpak installations and the
10
+ file readers it uses, so the rule can be checked from any system without touching the real session; the last one reads
11
+ the session.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import configparser
17
+ import os
18
+ import sys
19
+ from typing import TYPE_CHECKING
20
+
21
+ from gitpane.config import LINUX_PLATFORM
22
+ from gitpane.system.xdg_session import desktop_names, xdg_config_dirs, xdg_config_home, xdg_data_dirs, xdg_data_home
23
+ from gitpane.tool.editor_recipe import FLATPAK_EXPORTED_APPLICATION_PARTS, editor_platform, flatpak_installation_dirs
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Callable, Mapping
27
+
28
+ PLAIN_TEXT_MIME_TYPE = "text/plain"
29
+ DEFAULT_APPLICATIONS_SECTION = "Default Applications"
30
+ MIMEAPPS_BASENAME = "mimeapps.list"
31
+ DESKTOP_MIMEAPPS_SUFFIX = "-mimeapps.list"
32
+ # Not in the specification, but GLib still reads it in the data folders and older distributions ship it.
33
+ DEFAULTS_LIST_BASENAME = "defaults.list"
34
+ APPLICATIONS_DIR_BASENAME = "applications"
35
+ DESKTOP_ID_SEPARATOR = ";"
36
+ MIMEAPPS_ENCODING = "utf-8"
37
+ # A desktop file identifier names a file directly inside an applications folder; one with a slash names nothing there.
38
+ DESKTOP_ID_FORBIDDEN_SEPARATOR = "/"
39
+
40
+
41
+ def application_dirs(environment_by_name: Mapping[str, str], home_dir: str) -> list[str]:
42
+ """List the applications folders of the data folders, the user's first, where desktop files and the deprecated mimeapps.list live."""
43
+ application_folder_dirs = [os.path.join(xdg_data_home(environment_by_name, home_dir), APPLICATIONS_DIR_BASENAME)]
44
+ for data_dir in xdg_data_dirs(environment_by_name):
45
+ application_folder_dirs.append(os.path.join(data_dir, APPLICATIONS_DIR_BASENAME))
46
+ return application_folder_dirs
47
+
48
+
49
+ def desktop_file_dirs(environment_by_name: Mapping[str, str], home_dir: str, flatpak_dirs: tuple[str, ...]) -> list[str]:
50
+ """List the folders an installed application's desktop file can be in: the applications folders of the data folders, then the one each Flatpak installation exports.
51
+
52
+ The session adds the Flatpak export folders to its data folders only when it opens, so an editor installed from
53
+ Flatpak during the session is not in them yet; its installation already exports its desktop file.
54
+ """
55
+ folder_dirs = application_dirs(environment_by_name, home_dir)
56
+ for flatpak_dir in flatpak_dirs:
57
+ folder_dirs.append(os.path.join(flatpak_dir, *FLATPAK_EXPORTED_APPLICATION_PARTS))
58
+ return folder_dirs
59
+
60
+
61
+ def mimeapps_filenames(environment_by_name: Mapping[str, str], home_dir: str) -> list[str]:
62
+ """List the files that can name a default application, most important first.
63
+
64
+ The user's configuration folder, then each system configuration folder, then the applications folder of the user's
65
+ data folder, then that of each system data folder. In each folder, the file of each desktop named by
66
+ XDG_CURRENT_DESKTOP, most specific first, comes before the common file. The ``defaults.list`` of the data folders
67
+ come after all of them.
68
+ """
69
+ names = desktop_names(environment_by_name)
70
+ data_application_dirs = application_dirs(environment_by_name, home_dir)
71
+ searched_dirs = [xdg_config_home(environment_by_name, home_dir), *xdg_config_dirs(environment_by_name), *data_application_dirs]
72
+ filenames = []
73
+ for searched_dir in searched_dirs:
74
+ for name in names:
75
+ filenames.append(os.path.join(searched_dir, name + DESKTOP_MIMEAPPS_SUFFIX))
76
+ filenames.append(os.path.join(searched_dir, MIMEAPPS_BASENAME))
77
+ for data_application_dir in data_application_dirs:
78
+ filenames.append(os.path.join(data_application_dir, DEFAULTS_LIST_BASENAME))
79
+ return filenames
80
+
81
+
82
+ def default_desktop_ids(text: str) -> list[str]:
83
+ """List, in order, the desktop file identifiers one mimeapps.list text names as default for plain text, or none.
84
+
85
+ The keys keep their case, since MIME types are written in lower case and a differently written key is another key;
86
+ ``%`` is plain text, and a repeated group is merged as GLib does. A text that is not a key file gives nothing.
87
+ """
88
+ parser = configparser.ConfigParser(interpolation=None, strict=False, delimiters=("=",), comment_prefixes=("#",))
89
+ # The parser lowercases keys by default; keyfile keys are case sensitive.
90
+ parser.optionxform = str
91
+ try:
92
+ parser.read_string(text)
93
+ except configparser.Error:
94
+ return []
95
+ if not parser.has_option(DEFAULT_APPLICATIONS_SECTION, PLAIN_TEXT_MIME_TYPE):
96
+ return []
97
+ desktop_ids = []
98
+ for desktop_id in parser.get(DEFAULT_APPLICATIONS_SECTION, PLAIN_TEXT_MIME_TYPE).split(DESKTOP_ID_SEPARATOR):
99
+ stripped_id = desktop_id.strip()
100
+ if stripped_id:
101
+ desktop_ids.append(stripped_id)
102
+ return desktop_ids
103
+
104
+
105
+ def _desktop_file_exists(desktop_id: str, data_application_dirs: list[str], is_file: Callable[[str], bool]) -> bool:
106
+ """Tell whether a desktop file of that identifier is in one of the applications folders, so the application is still installed."""
107
+ if DESKTOP_ID_FORBIDDEN_SEPARATOR in desktop_id:
108
+ return False
109
+ for data_application_dir in data_application_dirs:
110
+ if is_file(os.path.join(data_application_dir, desktop_id)):
111
+ return True
112
+ return False
113
+
114
+
115
+ def desktop_text_editor_id(environment_by_name: Mapping[str, str], home_dir: str, platform: str, read_text: Callable[[str], str | None], is_file: Callable[[str], bool], *,
116
+ flatpak_dirs: tuple[str, ...] = ()) -> str:
117
+ """Answer the desktop file identifier of the application the desktop opens for plain text, or an empty text.
118
+
119
+ The first identifier, in the order of the files then of their line, whose desktop file is still installed decides;
120
+ an identifier whose application was removed is skipped for the next one, as the specification asks. Outside the
121
+ freedesktop systems no file is read and the text is empty. ``read_text`` answers None for a file it cannot read,
122
+ which is then skipped. A desktop file counts as installed in the applications folders of the data folders or in
123
+ the one each of ``flatpak_dirs`` exports. No program is started.
124
+ """
125
+ if editor_platform(platform) != LINUX_PLATFORM:
126
+ return ""
127
+ installed_desktop_file_dirs = desktop_file_dirs(environment_by_name, home_dir, flatpak_dirs)
128
+ for filename in mimeapps_filenames(environment_by_name, home_dir):
129
+ text = read_text(filename)
130
+ if text is None:
131
+ continue
132
+ for desktop_id in default_desktop_ids(text):
133
+ if _desktop_file_exists(desktop_id, installed_desktop_file_dirs, is_file):
134
+ return desktop_id
135
+ return ""
136
+
137
+
138
+ def read_text_file(filename: str) -> str | None:
139
+ """Read a mimeapps.list file as UTF-8 text, or answer None when it is missing, unreadable or not UTF-8."""
140
+ try:
141
+ with open(filename, encoding=MIMEAPPS_ENCODING) as stream:
142
+ return stream.read()
143
+ except (OSError, UnicodeDecodeError):
144
+ return None
145
+
146
+
147
+ def session_desktop_text_editor_id() -> str:
148
+ """Answer the desktop's plain text application for this session, read from its real files.
149
+
150
+ It reads the user's folders, which may sit on a network share that stops answering: call it on a thread of its own
151
+ only, never on the window's thread. The session's two Flatpak installations are looked in too, for an editor
152
+ installed from Flatpak since the session opened.
153
+ """
154
+ environment_by_name = dict(os.environ)
155
+ home_dir = os.path.expanduser("~")
156
+ return desktop_text_editor_id(environment_by_name, home_dir, sys.platform, read_text_file, os.path.isfile, flatpak_dirs=flatpak_installation_dirs(environment_by_name, home_dir))