thefuck-leeguoo 3.41__py2.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 (218) hide show
  1. thefuck/__init__.py +0 -0
  2. thefuck/ai.py +765 -0
  3. thefuck/argument_parser.py +96 -0
  4. thefuck/conf.py +141 -0
  5. thefuck/const.py +111 -0
  6. thefuck/corrector.py +92 -0
  7. thefuck/entrypoints/__init__.py +0 -0
  8. thefuck/entrypoints/alias.py +28 -0
  9. thefuck/entrypoints/fix_command.py +105 -0
  10. thefuck/entrypoints/main.py +50 -0
  11. thefuck/entrypoints/not_configured.py +201 -0
  12. thefuck/entrypoints/setup.py +227 -0
  13. thefuck/entrypoints/shell_logger.py +79 -0
  14. thefuck/exceptions.py +10 -0
  15. thefuck/logs.py +255 -0
  16. thefuck/output_readers/__init__.py +20 -0
  17. thefuck/output_readers/read_log.py +108 -0
  18. thefuck/output_readers/rerun.py +72 -0
  19. thefuck/output_readers/shell_logger.py +60 -0
  20. thefuck/rules/__init__.py +0 -0
  21. thefuck/rules/adb_unknown_command.py +54 -0
  22. thefuck/rules/ag_literal.py +10 -0
  23. thefuck/rules/apt_get.py +50 -0
  24. thefuck/rules/apt_get_search.py +14 -0
  25. thefuck/rules/apt_invalid_operation.py +63 -0
  26. thefuck/rules/apt_list_upgradable.py +16 -0
  27. thefuck/rules/apt_upgrade.py +16 -0
  28. thefuck/rules/aws_cli.py +17 -0
  29. thefuck/rules/az_cli.py +17 -0
  30. thefuck/rules/brew_cask_dependency.py +33 -0
  31. thefuck/rules/brew_install.py +24 -0
  32. thefuck/rules/brew_link.py +15 -0
  33. thefuck/rules/brew_reinstall.py +19 -0
  34. thefuck/rules/brew_uninstall.py +14 -0
  35. thefuck/rules/brew_unknown_command.py +82 -0
  36. thefuck/rules/brew_update_formula.py +12 -0
  37. thefuck/rules/cargo.py +6 -0
  38. thefuck/rules/cargo_no_command.py +15 -0
  39. thefuck/rules/cat_dir.py +14 -0
  40. thefuck/rules/cd_correction.py +61 -0
  41. thefuck/rules/cd_cs.py +21 -0
  42. thefuck/rules/cd_mkdir.py +21 -0
  43. thefuck/rules/cd_parent.py +16 -0
  44. thefuck/rules/chmod_x.py +15 -0
  45. thefuck/rules/choco_install.py +25 -0
  46. thefuck/rules/composer_not_command.py +22 -0
  47. thefuck/rules/conda_mistype.py +17 -0
  48. thefuck/rules/cp_create_destination.py +15 -0
  49. thefuck/rules/cp_omitting_directory.py +15 -0
  50. thefuck/rules/cpp11.py +12 -0
  51. thefuck/rules/dirty_untar.py +53 -0
  52. thefuck/rules/dirty_unzip.py +60 -0
  53. thefuck/rules/django_south_ghost.py +8 -0
  54. thefuck/rules/django_south_merge.py +8 -0
  55. thefuck/rules/dnf_no_such_command.py +37 -0
  56. thefuck/rules/docker_image_being_used_by_container.py +20 -0
  57. thefuck/rules/docker_login.py +13 -0
  58. thefuck/rules/docker_not_command.py +49 -0
  59. thefuck/rules/dry.py +15 -0
  60. thefuck/rules/fab_command_not_found.py +38 -0
  61. thefuck/rules/fix_alt_space.py +15 -0
  62. thefuck/rules/fix_file.py +80 -0
  63. thefuck/rules/gem_unknown_command.py +36 -0
  64. thefuck/rules/git_add.py +27 -0
  65. thefuck/rules/git_add_force.py +13 -0
  66. thefuck/rules/git_bisect_usage.py +16 -0
  67. thefuck/rules/git_branch_0flag.py +24 -0
  68. thefuck/rules/git_branch_delete.py +13 -0
  69. thefuck/rules/git_branch_delete_checked_out.py +19 -0
  70. thefuck/rules/git_branch_exists.py +25 -0
  71. thefuck/rules/git_branch_list.py +14 -0
  72. thefuck/rules/git_checkout.py +49 -0
  73. thefuck/rules/git_clone_git_clone.py +12 -0
  74. thefuck/rules/git_clone_missing.py +42 -0
  75. thefuck/rules/git_commit_add.py +17 -0
  76. thefuck/rules/git_commit_amend.py +11 -0
  77. thefuck/rules/git_commit_reset.py +11 -0
  78. thefuck/rules/git_diff_no_index.py +16 -0
  79. thefuck/rules/git_diff_staged.py +13 -0
  80. thefuck/rules/git_fix_stash.py +37 -0
  81. thefuck/rules/git_flag_after_filename.py +31 -0
  82. thefuck/rules/git_help_aliased.py +12 -0
  83. thefuck/rules/git_hook_bypass.py +27 -0
  84. thefuck/rules/git_lfs_mistype.py +18 -0
  85. thefuck/rules/git_main_master.py +16 -0
  86. thefuck/rules/git_merge.py +18 -0
  87. thefuck/rules/git_merge_unrelated.py +12 -0
  88. thefuck/rules/git_not_command.py +18 -0
  89. thefuck/rules/git_pull.py +16 -0
  90. thefuck/rules/git_pull_clone.py +13 -0
  91. thefuck/rules/git_pull_uncommitted_changes.py +14 -0
  92. thefuck/rules/git_push.py +44 -0
  93. thefuck/rules/git_push_different_branch_names.py +12 -0
  94. thefuck/rules/git_push_force.py +18 -0
  95. thefuck/rules/git_push_pull.py +20 -0
  96. thefuck/rules/git_push_without_commits.py +12 -0
  97. thefuck/rules/git_rebase_merge_dir.py +17 -0
  98. thefuck/rules/git_rebase_no_changes.py +13 -0
  99. thefuck/rules/git_remote_delete.py +13 -0
  100. thefuck/rules/git_remote_seturl_add.py +12 -0
  101. thefuck/rules/git_rm_local_modifications.py +19 -0
  102. thefuck/rules/git_rm_recursive.py +16 -0
  103. thefuck/rules/git_rm_staged.py +19 -0
  104. thefuck/rules/git_stash.py +15 -0
  105. thefuck/rules/git_stash_pop.py +18 -0
  106. thefuck/rules/git_tag_force.py +13 -0
  107. thefuck/rules/git_two_dashes.py +14 -0
  108. thefuck/rules/go_run.py +16 -0
  109. thefuck/rules/go_unknown_command.py +28 -0
  110. thefuck/rules/gradle_no_task.py +34 -0
  111. thefuck/rules/gradle_wrapper.py +13 -0
  112. thefuck/rules/grep_arguments_order.py +23 -0
  113. thefuck/rules/grep_recursive.py +10 -0
  114. thefuck/rules/grunt_task_not_found.py +37 -0
  115. thefuck/rules/gulp_not_task.py +22 -0
  116. thefuck/rules/has_exists_script.py +13 -0
  117. thefuck/rules/heroku_multiple_apps.py +12 -0
  118. thefuck/rules/heroku_not_command.py +11 -0
  119. thefuck/rules/history.py +15 -0
  120. thefuck/rules/hostscli.py +27 -0
  121. thefuck/rules/ifconfig_device_not_found.py +23 -0
  122. thefuck/rules/java.py +17 -0
  123. thefuck/rules/javac.py +18 -0
  124. thefuck/rules/lein_not_task.py +19 -0
  125. thefuck/rules/ln_no_hard_link.py +23 -0
  126. thefuck/rules/ln_s_order.py +26 -0
  127. thefuck/rules/long_form_help.py +27 -0
  128. thefuck/rules/ls_all.py +10 -0
  129. thefuck/rules/ls_lah.py +12 -0
  130. thefuck/rules/man.py +33 -0
  131. thefuck/rules/man_no_space.py +10 -0
  132. thefuck/rules/mercurial.py +27 -0
  133. thefuck/rules/missing_space_before_subcommand.py +21 -0
  134. thefuck/rules/mkdir_p.py +13 -0
  135. thefuck/rules/mvn_no_command.py +11 -0
  136. thefuck/rules/mvn_unknown_lifecycle_phase.py +30 -0
  137. thefuck/rules/nixos_cmd_not_found.py +15 -0
  138. thefuck/rules/no_command.py +41 -0
  139. thefuck/rules/no_such_file.py +30 -0
  140. thefuck/rules/npm_missing_script.py +17 -0
  141. thefuck/rules/npm_run_script.py +17 -0
  142. thefuck/rules/npm_wrong_command.py +42 -0
  143. thefuck/rules/omnienv_no_such_command.py +35 -0
  144. thefuck/rules/open.py +40 -0
  145. thefuck/rules/pacman.py +17 -0
  146. thefuck/rules/pacman_invalid_option.py +20 -0
  147. thefuck/rules/pacman_not_found.py +26 -0
  148. thefuck/rules/path_from_history.py +53 -0
  149. thefuck/rules/php_s.py +11 -0
  150. thefuck/rules/pip_install.py +15 -0
  151. thefuck/rules/pip_unknown_command.py +19 -0
  152. thefuck/rules/port_already_in_use.py +40 -0
  153. thefuck/rules/prove_recursively.py +27 -0
  154. thefuck/rules/python_command.py +17 -0
  155. thefuck/rules/python_execute.py +15 -0
  156. thefuck/rules/python_module_error.py +13 -0
  157. thefuck/rules/quotation_marks.py +12 -0
  158. thefuck/rules/rails_migrations_pending.py +14 -0
  159. thefuck/rules/react_native_command_unrecognized.py +34 -0
  160. thefuck/rules/remove_shell_prompt_literal.py +23 -0
  161. thefuck/rules/remove_trailing_cedilla.py +11 -0
  162. thefuck/rules/rm_dir.py +16 -0
  163. thefuck/rules/rm_root.py +16 -0
  164. thefuck/rules/scm_correction.py +32 -0
  165. thefuck/rules/sed_unterminated_s.py +18 -0
  166. thefuck/rules/sl_ls.py +14 -0
  167. thefuck/rules/ssh_known_hosts.py +37 -0
  168. thefuck/rules/sudo.py +47 -0
  169. thefuck/rules/sudo_command_from_user_path.py +21 -0
  170. thefuck/rules/switch_lang.py +117 -0
  171. thefuck/rules/systemctl.py +22 -0
  172. thefuck/rules/terraform_init.py +13 -0
  173. thefuck/rules/terraform_no_command.py +16 -0
  174. thefuck/rules/test.py.py +10 -0
  175. thefuck/rules/tmux.py +18 -0
  176. thefuck/rules/touch.py +14 -0
  177. thefuck/rules/tsuru_login.py +12 -0
  178. thefuck/rules/tsuru_not_command.py +15 -0
  179. thefuck/rules/unknown_command.py +13 -0
  180. thefuck/rules/unsudo.py +15 -0
  181. thefuck/rules/vagrant_up.py +21 -0
  182. thefuck/rules/whois.py +34 -0
  183. thefuck/rules/workon_doesnt_exists.py +32 -0
  184. thefuck/rules/wrong_hyphen_before_subcommand.py +20 -0
  185. thefuck/rules/yarn_alias.py +14 -0
  186. thefuck/rules/yarn_command_not_found.py +43 -0
  187. thefuck/rules/yarn_command_replaced.py +13 -0
  188. thefuck/rules/yarn_help.py +17 -0
  189. thefuck/rules/yum_invalid_operation.py +39 -0
  190. thefuck/shells/__init__.py +52 -0
  191. thefuck/shells/bash.py +94 -0
  192. thefuck/shells/fish.py +131 -0
  193. thefuck/shells/generic.py +154 -0
  194. thefuck/shells/powershell.py +43 -0
  195. thefuck/shells/tcsh.py +44 -0
  196. thefuck/shells/zsh.py +98 -0
  197. thefuck/specific/__init__.py +0 -0
  198. thefuck/specific/apt.py +3 -0
  199. thefuck/specific/archlinux.py +48 -0
  200. thefuck/specific/brew.py +15 -0
  201. thefuck/specific/dnf.py +3 -0
  202. thefuck/specific/git.py +32 -0
  203. thefuck/specific/nix.py +3 -0
  204. thefuck/specific/npm.py +21 -0
  205. thefuck/specific/sudo.py +18 -0
  206. thefuck/specific/yum.py +3 -0
  207. thefuck/system/__init__.py +7 -0
  208. thefuck/system/unix.py +57 -0
  209. thefuck/system/win32.py +43 -0
  210. thefuck/types.py +261 -0
  211. thefuck/ui.py +116 -0
  212. thefuck/utils.py +385 -0
  213. thefuck_leeguoo-3.41.dist-info/METADATA +681 -0
  214. thefuck_leeguoo-3.41.dist-info/RECORD +218 -0
  215. thefuck_leeguoo-3.41.dist-info/WHEEL +6 -0
  216. thefuck_leeguoo-3.41.dist-info/entry_points.txt +3 -0
  217. thefuck_leeguoo-3.41.dist-info/licenses/LICENSE.md +22 -0
  218. thefuck_leeguoo-3.41.dist-info/top_level.txt +1 -0
@@ -0,0 +1,201 @@
1
+ # Initialize output before importing any module, that can use colorama.
2
+ from ..system import init_output
3
+
4
+ init_output()
5
+
6
+ import getpass # noqa: E402
7
+ import os # noqa: E402
8
+ import json # noqa: E402
9
+ import sys # noqa: E402
10
+ import subprocess # noqa: E402
11
+ from tempfile import gettempdir # noqa: E402
12
+ import time # noqa: E402
13
+ import six # noqa: E402
14
+ from psutil import Process # noqa: E402
15
+ from .. import logs, const, types # noqa: E402
16
+ from ..shells import shell # noqa: E402
17
+ from ..conf import settings # noqa: E402
18
+ from ..system import Path # noqa: E402
19
+ from ..corrector import get_corrected_commands # noqa: E402
20
+ from ..exceptions import EmptyCommand # noqa: E402
21
+
22
+
23
+ def _get_shell_pid():
24
+ """Returns parent process pid."""
25
+ proc = Process(os.getpid())
26
+
27
+ try:
28
+ return proc.parent().pid
29
+ except TypeError:
30
+ return proc.parent.pid
31
+
32
+
33
+ def _get_not_configured_usage_tracker_path():
34
+ """Returns path of special file where we store latest shell pid."""
35
+ return Path(gettempdir()).joinpath(u'thefuck.last_not_configured_run_{}'.format(
36
+ getpass.getuser(),
37
+ ))
38
+
39
+
40
+ def _record_first_run():
41
+ """Records shell pid to tracker file."""
42
+ info = {'pid': _get_shell_pid(),
43
+ 'time': time.time()}
44
+
45
+ mode = 'wb' if six.PY2 else 'w'
46
+ with _get_not_configured_usage_tracker_path().open(mode) as tracker:
47
+ json.dump(info, tracker)
48
+
49
+
50
+ def _get_previous_command():
51
+ history = shell.get_history()
52
+
53
+ if history:
54
+ return history[-1]
55
+ else:
56
+ return None
57
+
58
+
59
+ def _is_second_run():
60
+ """Returns `True` when we know that `fuck` called second time."""
61
+ tracker_path = _get_not_configured_usage_tracker_path()
62
+ if not tracker_path.exists():
63
+ return False
64
+
65
+ current_pid = _get_shell_pid()
66
+ with tracker_path.open('r') as tracker:
67
+ try:
68
+ info = json.load(tracker)
69
+ except ValueError:
70
+ return False
71
+
72
+ if not (isinstance(info, dict) and info.get('pid') == current_pid):
73
+ return False
74
+
75
+ return (_get_previous_command() == 'fuck' or
76
+ time.time() - info.get('time', 0) < const.CONFIGURATION_TIMEOUT)
77
+
78
+
79
+ def _is_already_configured(configuration_details):
80
+ """Returns `True` when alias already in shell config."""
81
+ path = Path(configuration_details.path).expanduser()
82
+ with path.open('r') as shell_config:
83
+ return configuration_details.content in shell_config.read()
84
+
85
+
86
+ def _configure(configuration_details):
87
+ """Adds alias to shell config."""
88
+ path = Path(configuration_details.path).expanduser()
89
+ with path.open('a') as shell_config:
90
+ shell_config.write(u'\n')
91
+ shell_config.write(configuration_details.content)
92
+ shell_config.write(u'\n')
93
+
94
+
95
+ def _get_last_command_from_history():
96
+ """Gets the last command from shell history, excluding 'fuck' itself."""
97
+ history = shell.get_history()
98
+ if not history:
99
+ return None
100
+
101
+ # Find the last command that isn't 'fuck'
102
+ for cmd in reversed(history):
103
+ cmd_stripped = cmd.strip()
104
+ if cmd_stripped and cmd_stripped != 'fuck' and not cmd_stripped.startswith('fuck '):
105
+ return cmd_stripped
106
+ return None
107
+
108
+
109
+ def _run_and_fix_command(command_script):
110
+ """Runs the command and tries to fix it."""
111
+ try:
112
+ command = types.Command.from_raw_script([command_script])
113
+ except EmptyCommand:
114
+ logs.debug('Empty command, nothing to do')
115
+ return False
116
+
117
+ corrected_commands = get_corrected_commands(command)
118
+
119
+ # Get the first corrected command
120
+ selected_command = None
121
+ for cmd in corrected_commands:
122
+ selected_command = cmd
123
+ break
124
+
125
+ if selected_command:
126
+ # Print the fixed command
127
+ fixed_script = selected_command.script
128
+ print(u'\nSuggested fix: {}'.format(fixed_script))
129
+
130
+ # Ask for confirmation
131
+ try:
132
+ if six.PY2:
133
+ response = raw_input('Run this command? [Y/n] ') # noqa: F821
134
+ else:
135
+ response = input('Run this command? [Y/n] ')
136
+ if response.lower() in ('', 'y', 'yes'):
137
+ subprocess.call(fixed_script, shell=True)
138
+ return True
139
+ except (KeyboardInterrupt, EOFError):
140
+ print('')
141
+ return True
142
+ return False
143
+
144
+
145
+ def _auto_configure_shell():
146
+ """Automatically configures the shell if possible."""
147
+ configuration_details = shell.how_to_configure()
148
+ if (configuration_details and
149
+ configuration_details.can_configure_automatically and
150
+ not _is_already_configured(configuration_details)):
151
+ _configure(configuration_details)
152
+ return True
153
+ return False
154
+
155
+
156
+ def main():
157
+ """Main entry point for 'fuck' command.
158
+
159
+ This now works in two modes:
160
+ 1. If shell alias is configured: works through the alias (fast mode)
161
+ 2. If not configured: reads history, reruns command, and fixes it
162
+
163
+ Also auto-configures the shell for better experience next time.
164
+ """
165
+ settings.init()
166
+
167
+ # Check if we're being called through the alias (TF_COMMAND is set)
168
+ if os.environ.get('TF_COMMAND'):
169
+ # We're being called through alias, delegate to fix_command
170
+ from .fix_command import fix_command
171
+ from ..argument_parser import Parser
172
+ parser = Parser()
173
+ known_args = parser.parse(sys.argv)
174
+ fix_command(known_args)
175
+ return
176
+
177
+ # Not called through alias - work directly with history
178
+ last_command = _get_last_command_from_history()
179
+
180
+ if not last_command:
181
+ logs.how_to_configure_alias(shell.how_to_configure())
182
+ return
183
+
184
+ # Try to fix the command
185
+ print(u'Last command: {}'.format(last_command))
186
+ print(u'Re-running to get output...')
187
+
188
+ if _run_and_fix_command(last_command):
189
+ # Auto-configure shell for next time
190
+ if _auto_configure_shell():
191
+ print(u'\nShell configured! Restart your terminal for faster experience.')
192
+ else:
193
+ print(u'No fix found for this command.')
194
+
195
+ # Show configuration help
196
+ configuration_details = shell.how_to_configure()
197
+ if configuration_details:
198
+ if _auto_configure_shell():
199
+ print(u'\nShell auto-configured! Restart your terminal.')
200
+ else:
201
+ logs.how_to_configure_alias(configuration_details)
@@ -0,0 +1,227 @@
1
+ import os
2
+ import sys
3
+ import shutil
4
+ from getpass import getpass
5
+ import six
6
+ from ..conf import settings
7
+ from ..logs import warn
8
+ from ..shells import shell
9
+ from ..system import Path
10
+
11
+ # Use builtin input to avoid colorama wrapper issues
12
+ if six.PY2:
13
+ _input = raw_input # noqa: F821
14
+ else:
15
+ import builtins
16
+ _input = builtins.input
17
+
18
+
19
+ def _ask(prompt, default=None, secret=False):
20
+ if default:
21
+ prompt_text = '{} [{}]: '.format(prompt, default)
22
+ else:
23
+ prompt_text = '{}: '.format(prompt)
24
+ if secret:
25
+ value = getpass(prompt_text)
26
+ else:
27
+ value = _input(prompt_text)
28
+ value = value.strip()
29
+ if not value:
30
+ return default
31
+ return value
32
+
33
+
34
+ def _ask_bool(prompt, default):
35
+ suffix = 'Y/n' if default else 'y/N'
36
+ while True:
37
+ value = _input('{} ({})? '.format(prompt, suffix)).strip().lower()
38
+ if not value:
39
+ return default
40
+ if value in ('y', 'yes'):
41
+ return True
42
+ if value in ('n', 'no'):
43
+ return False
44
+ sys.stderr.write('Please enter y or n.\n')
45
+
46
+
47
+ def _ask_int(prompt, default):
48
+ value = _ask(prompt, six.text_type(default))
49
+ try:
50
+ return int(value)
51
+ except (TypeError, ValueError):
52
+ warn('Invalid number, using default {}'.format(default))
53
+ return default
54
+
55
+
56
+ def _normalize_mode(mode, default):
57
+ if mode in ('prefer', 'fallback'):
58
+ return mode
59
+ warn("Unknown AI mode '{}', using {}".format(mode, default))
60
+ return default
61
+
62
+
63
+ def _quote(value):
64
+ return shell.quote(six.text_type(value))
65
+
66
+
67
+ def _is_fish():
68
+ return shell.__class__.__name__.lower() == 'fish'
69
+
70
+
71
+ def _build_env_lines(values, fish):
72
+ lines = []
73
+ for key, value in values.items():
74
+ if value is None:
75
+ continue
76
+ if fish:
77
+ lines.append('set -gx {} {}'.format(key, _quote(value)))
78
+ else:
79
+ lines.append('export {}={}'.format(key, _quote(value)))
80
+ return lines
81
+
82
+
83
+ def _shell_path(path, xdg_config_home, use_xdg):
84
+ if use_xdg and xdg_config_home:
85
+ xdg_root = Path(xdg_config_home, 'thefuck').expanduser()
86
+ if path == xdg_root:
87
+ return '$XDG_CONFIG_HOME/thefuck'
88
+ if str(path).startswith(str(xdg_root) + os.sep):
89
+ suffix = str(path)[len(str(xdg_root)) + 1:]
90
+ return '$XDG_CONFIG_HOME/thefuck/{}'.format(suffix)
91
+ home = Path('~').expanduser()
92
+ if str(path).startswith(str(home) + os.sep):
93
+ suffix = str(path)[len(str(home)) + 1:]
94
+ return '$HOME/{}'.format(suffix)
95
+ return str(path)
96
+
97
+
98
+ def _write_env_file(path, lines):
99
+ with path.open(mode='w') as env_file:
100
+ env_file.write('# thefuck env\n')
101
+ for line in lines:
102
+ env_file.write(line + '\n')
103
+
104
+
105
+ def _build_path_lines(bin_path, fish):
106
+ if fish:
107
+ return ['set -gx PATH {} $PATH'.format(bin_path)]
108
+ return ['export PATH="{}:$PATH"'.format(bin_path)]
109
+
110
+
111
+ def _write_wrapper(path):
112
+ content = (
113
+ '#!/bin/sh\n'
114
+ 'if [ -n "$THEFUCK_PROJECT_PATH" ]; then\n'
115
+ ' exec uv run --project "$THEFUCK_PROJECT_PATH" thefuck "$@"\n'
116
+ 'fi\n'
117
+ 'if command -v uvx >/dev/null 2>&1; then\n'
118
+ ' exec uvx thefuck "$@"\n'
119
+ 'fi\n'
120
+ 'exec uv run thefuck "$@"\n'
121
+ )
122
+ with path.open(mode='w') as script_file:
123
+ script_file.write(content)
124
+ os.chmod(str(path), 0o755)
125
+
126
+
127
+ def setup():
128
+ print('Setup:')
129
+ print('Press enter to keep the default value shown in brackets.')
130
+
131
+ xdg_config_home_env = os.environ.get('XDG_CONFIG_HOME')
132
+ xdg_config_home = xdg_config_home_env or '~/.config'
133
+ use_xdg = xdg_config_home_env is not None
134
+ config_root = Path(xdg_config_home, 'thefuck').expanduser()
135
+ legacy_root = Path('~/.thefuck').expanduser()
136
+ if legacy_root.is_dir() and legacy_root != config_root:
137
+ if _ask_bool(
138
+ 'Legacy config found at {}. Migrate to {}'
139
+ .format(legacy_root, config_root),
140
+ True):
141
+ try:
142
+ if not config_root.parent.is_dir():
143
+ config_root.parent.mkdir(parents=True)
144
+ shutil.move(str(legacy_root), str(config_root))
145
+ except OSError as exc:
146
+ warn('Failed to migrate config: {}'.format(exc))
147
+ config_root = legacy_root
148
+ else:
149
+ config_root = legacy_root
150
+ bin_dir = config_root.joinpath('bin')
151
+ fish = _is_fish()
152
+ env_path = config_root.joinpath('env.fish' if fish else 'env.sh')
153
+ env_path_shell = _shell_path(env_path, xdg_config_home, use_xdg)
154
+ bin_path_shell = _shell_path(bin_dir, xdg_config_home, use_xdg)
155
+
156
+ if not config_root.is_dir():
157
+ config_root.mkdir(parents=True)
158
+ if not bin_dir.is_dir():
159
+ bin_dir.mkdir(parents=True)
160
+
161
+ settings.init()
162
+ defaults = {
163
+ 'ai_enabled': settings.ai_enabled,
164
+ 'ai_url': settings.ai_url,
165
+ 'ai_token': settings.ai_token,
166
+ 'ai_model': settings.ai_model,
167
+ 'ai_timeout': settings.ai_timeout,
168
+ 'ai_reasoning_effort': settings.ai_reasoning_effort,
169
+ 'ai_stream': settings.ai_stream,
170
+ 'ai_mode': settings.ai_mode,
171
+ 'ai_stream_output': settings.ai_stream_output
172
+ }
173
+
174
+ ai_enabled = _ask_bool('Enable AI', defaults['ai_enabled'])
175
+ values = {
176
+ 'THEFUCK_AI_ENABLED': 'true' if ai_enabled else 'false'
177
+ }
178
+
179
+ if ai_enabled:
180
+ ai_url = _ask('AI URL', defaults['ai_url'])
181
+ ai_token = _ask('AI token', defaults['ai_token'], secret=False)
182
+ ai_model = _ask('AI model', defaults['ai_model'])
183
+ ai_timeout = _ask_int('Timeout seconds', defaults['ai_timeout'])
184
+ ai_reasoning_effort = _ask(
185
+ 'Reasoning effort (low/medium/high)',
186
+ defaults['ai_reasoning_effort'])
187
+ ai_stream = _ask_bool('Use SSE stream', defaults['ai_stream'])
188
+ ai_stream_output = _ask_bool(
189
+ 'Stream output while waiting', defaults['ai_stream_output'])
190
+ ai_mode = _normalize_mode(
191
+ _ask('AI mode (prefer/fallback)', defaults['ai_mode']),
192
+ defaults['ai_mode'])
193
+
194
+ values.update({
195
+ 'THEFUCK_AI_URL': ai_url,
196
+ 'THEFUCK_AI_TOKEN': ai_token,
197
+ 'THEFUCK_AI_MODEL': ai_model,
198
+ 'THEFUCK_AI_TIMEOUT': ai_timeout,
199
+ 'THEFUCK_AI_REASONING_EFFORT': ai_reasoning_effort,
200
+ 'THEFUCK_AI_STREAM': 'true' if ai_stream else 'false',
201
+ 'THEFUCK_AI_MODE': ai_mode,
202
+ 'THEFUCK_AI_STREAM_OUTPUT': 'true' if ai_stream_output else 'false'
203
+ })
204
+
205
+ env_lines = _build_path_lines(bin_path_shell, fish)
206
+ env_lines.extend(_build_env_lines(values, fish))
207
+ _write_env_file(env_path, env_lines)
208
+ _write_wrapper(bin_dir.joinpath('thefuck'))
209
+
210
+ print('\nWrote env file:', env_path)
211
+ print('Wrote wrapper:', bin_dir.joinpath('thefuck'))
212
+ print('\nAdd to your shell config:')
213
+ print('source {}'.format(env_path_shell))
214
+ print('\nOptional for local development:')
215
+ print('set THEFUCK_PROJECT_PATH to use a local checkout with uv run')
216
+
217
+ config = shell.how_to_configure()
218
+ if config and _ask_bool(
219
+ 'Append setup to {}'.format(config.path), False):
220
+ path = os.path.expanduser(config.path)
221
+ try:
222
+ with open(path, 'a') as config_file:
223
+ config_file.write('\n# thefuck config\n')
224
+ config_file.write('source {}\n'.format(env_path_shell))
225
+ print('Done. Run: {}'.format(config.reload))
226
+ except OSError as exc:
227
+ warn('Failed to write {}: {}'.format(path, exc))
@@ -0,0 +1,79 @@
1
+ import array
2
+ import fcntl
3
+ from functools import partial
4
+ import mmap
5
+ import os
6
+ import pty
7
+ import signal
8
+ import sys
9
+ import termios
10
+ import tty
11
+ from .. import logs, const
12
+
13
+
14
+ def _read(f, fd):
15
+ data = os.read(fd, 1024)
16
+ try:
17
+ f.write(data)
18
+ except ValueError:
19
+ position = const.LOG_SIZE_IN_BYTES - const.LOG_SIZE_TO_CLEAN
20
+ f.move(0, const.LOG_SIZE_TO_CLEAN, position)
21
+ f.seek(position)
22
+ f.write(b'\x00' * const.LOG_SIZE_TO_CLEAN)
23
+ f.seek(position)
24
+ return data
25
+
26
+
27
+ def _set_pty_size(master_fd):
28
+ buf = array.array('h', [0, 0, 0, 0])
29
+ fcntl.ioctl(pty.STDOUT_FILENO, termios.TIOCGWINSZ, buf, True)
30
+ fcntl.ioctl(master_fd, termios.TIOCSWINSZ, buf)
31
+
32
+
33
+ def _spawn(shell, master_read):
34
+ """Create a spawned process.
35
+
36
+ Modified version of pty.spawn with terminal size support.
37
+
38
+ """
39
+ pid, master_fd = pty.fork()
40
+
41
+ if pid == pty.CHILD:
42
+ os.execlp(shell, shell)
43
+
44
+ try:
45
+ mode = tty.tcgetattr(pty.STDIN_FILENO)
46
+ tty.setraw(pty.STDIN_FILENO)
47
+ restore = True
48
+ except tty.error: # This is the same as termios.error
49
+ restore = False
50
+
51
+ _set_pty_size(master_fd)
52
+ signal.signal(signal.SIGWINCH, lambda *_: _set_pty_size(master_fd))
53
+
54
+ try:
55
+ pty._copy(master_fd, master_read, pty._read)
56
+ except OSError:
57
+ if restore:
58
+ tty.tcsetattr(pty.STDIN_FILENO, tty.TCSAFLUSH, mode)
59
+
60
+ os.close(master_fd)
61
+ return os.waitpid(pid, 0)[1]
62
+
63
+
64
+ def shell_logger(output):
65
+ """Logs shell output to the `output`.
66
+
67
+ Works like unix script command with `-f` flag.
68
+
69
+ """
70
+ if not os.environ.get('SHELL'):
71
+ logs.warn("Shell logger doesn't support your platform.")
72
+ sys.exit(1)
73
+
74
+ fd = os.open(output, os.O_CREAT | os.O_TRUNC | os.O_RDWR)
75
+ os.write(fd, b'\x00' * const.LOG_SIZE_IN_BYTES)
76
+ buffer = mmap.mmap(fd, const.LOG_SIZE_IN_BYTES, mmap.MAP_SHARED, mmap.PROT_WRITE)
77
+ return_code = _spawn(os.environ['SHELL'], partial(_read, buffer))
78
+
79
+ sys.exit(return_code)
thefuck/exceptions.py ADDED
@@ -0,0 +1,10 @@
1
+ class EmptyCommand(Exception):
2
+ """Raised when empty command passed to `thefuck`."""
3
+
4
+
5
+ class NoRuleMatched(Exception):
6
+ """Raised when no rule matched for some command."""
7
+
8
+
9
+ class ScriptNotInLog(Exception):
10
+ """Script not found in log."""