easyrip 4.13.2__py3-none-any.whl → 4.14.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.
easyrip/__main__.py CHANGED
@@ -23,7 +23,12 @@ from .easyrip_command import (
23
23
  )
24
24
  from .easyrip_config.config import Config_key, config
25
25
  from .easyrip_main import Ripper, get_input_prompt, init, log, run_command
26
- from .easyrip_prompt import ConfigFileHistory, SmartPathCompleter, easyrip_prompt
26
+ from .easyrip_prompt import (
27
+ ConfigFileHistory,
28
+ CustomPromptCompleter,
29
+ SmartPathCompleter,
30
+ easyrip_prompt,
31
+ )
27
32
  from .global_val import C_D, C_Z
28
33
 
29
34
 
@@ -128,6 +133,7 @@ def run() -> NoReturn:
128
133
  (
129
134
  _ctv_to_nc(cmd_ctv_tuple),
130
135
  OptCompleter(opt_tree=_ctv_to_nd(ct.value for ct in Opt_type)),
136
+ CustomPromptCompleter(),
131
137
  )
132
138
  ),
133
139
  history=prompt_history,
@@ -253,12 +253,22 @@ class Cmd_type(enum.Enum):
253
253
  description=(
254
254
  "history\n" # .
255
255
  " Show prompt history\n"
256
- "clear | clean\n"
257
- " Delete history file"
256
+ "history_clear\n"
257
+ " Delete history file\n"
258
+ "add <name:string> <cmd:string>\n"
259
+ " Add a custom prompt\n"
260
+ " e.g. prompt add myprompt echo my prompt\n"
261
+ "del <name:string>\n"
262
+ " Delete a custom prompt"
263
+ "show\n"
264
+ " Show custom prompt"
258
265
  ),
259
266
  childs=(
260
267
  Cmd_type_val(("history",)),
261
- Cmd_type_val(("clear", "clean")),
268
+ Cmd_type_val(("history_clear",)),
269
+ Cmd_type_val(("add",)),
270
+ Cmd_type_val(("del",)),
271
+ Cmd_type_val(("show",)),
262
272
  ),
263
273
  )
264
274
  translate = Cmd_type_val(
easyrip/easyrip_main.py CHANGED
@@ -345,7 +345,7 @@ def run_ripper_list(
345
345
  log.info("Execute shutdown in {}s", shutdown_sec)
346
346
  if os.name == "nt":
347
347
  _cmd = (
348
- f'shutdown /s /t {shutdown_sec} /c "{gettext("{} run completed, shutdown in {}s", PROJECT_TITLE, shutdown_sec)}"',
348
+ f'shutdown /s /t {shutdown_sec} /d p:4:0 /c "{gettext("{} run completed, shutdown in {}s", PROJECT_TITLE, shutdown_sec)}"',
349
349
  )
350
350
  elif os.name == "posix":
351
351
  _cmd = (f"shutdown -h +{shutdown_sec // 60}",)
@@ -775,8 +775,36 @@ def run_command(command: Iterable[str] | str) -> bool:
775
775
  ) as f:
776
776
  for line in f.read().splitlines():
777
777
  log.send(line, is_format=False)
778
- case "clear" | "clean":
779
- easyrip_prompt.clear()
778
+
779
+ case "history_clear":
780
+ easyrip_prompt.clear_history()
781
+
782
+ case "add":
783
+ _name = cmd_list[2]
784
+ _cmd = (
785
+ command.split(maxsplit=3)[-1]
786
+ if isinstance(command, str)
787
+ else " ".join(cmd_list[3:])
788
+ )
789
+ if " " in _name:
790
+ log.error("The name must not contain spaces")
791
+ return False
792
+ log.info("Add custom prompt {!r} = {!r}", _name, _cmd)
793
+ return easyrip_prompt.add_custom_prompt(_name, _cmd)
794
+
795
+ case "del":
796
+ _name = cmd_list[2]
797
+ log.info("Del custom prompt {!r}", _name)
798
+ return easyrip_prompt.del_custom_prompt(_name)
799
+
800
+ case "show":
801
+ if _custom_prompt := easyrip_prompt.get_custom_prompt():
802
+ log.send(
803
+ "\n".join(
804
+ f"{k!r} = {v!r}" for k, v in _custom_prompt.items()
805
+ ),
806
+ is_format=False,
807
+ )
780
808
 
781
809
  case Cmd_type.translate:
782
810
  if not (_infix := cmd_list[1]):
easyrip/easyrip_prompt.py CHANGED
@@ -1,5 +1,6 @@
1
1
  import os
2
2
  import re
3
+ import tomllib
3
4
  from collections.abc import Iterable
4
5
 
5
6
  from prompt_toolkit.completion import CompleteEvent, Completer, Completion
@@ -8,15 +9,73 @@ from prompt_toolkit.formatted_text import StyleAndTextTuples
8
9
  from prompt_toolkit.history import FileHistory
9
10
 
10
11
  from .global_val import C_Z, get_CONFIG_DIR
12
+ from .utils import type_match
11
13
 
12
14
 
13
15
  class easyrip_prompt:
14
16
  PROMPT_HISTORY_FILE = get_CONFIG_DIR() / "prompt_history.txt"
17
+ PROMPT_CUSTOM_FILE = get_CONFIG_DIR() / "prompt_custom.toml"
18
+
19
+ __prompt_custom_data: dict[str, str] | None = None
15
20
 
16
21
  @classmethod
17
- def clear(cls) -> None:
22
+ def clear_history(cls) -> None:
18
23
  cls.PROMPT_HISTORY_FILE.unlink(True)
19
24
 
25
+ @classmethod
26
+ def get_custom_prompt(cls) -> dict[str, str]:
27
+ if cls.__prompt_custom_data is not None:
28
+ return cls.__prompt_custom_data
29
+
30
+ cls.PROMPT_CUSTOM_FILE.touch()
31
+ with cls.PROMPT_CUSTOM_FILE.open("rb") as f:
32
+ data = tomllib.load(f)
33
+ assert type_match(data, dict[str, str])
34
+ cls.__prompt_custom_data = data
35
+ return data
36
+
37
+ @classmethod
38
+ def update_custom_prompt(cls, data: dict[str, str]) -> bool:
39
+ cls.PROMPT_CUSTOM_FILE.touch()
40
+ with cls.PROMPT_CUSTOM_FILE.open("wt", encoding="utf-8", newline="\n") as f:
41
+ f.writelines(f"{k!r} = {v!r}\n" for k, v in data.items())
42
+ cls.__prompt_custom_data = None
43
+ return True
44
+
45
+ @classmethod
46
+ def add_custom_prompt(cls, name: str, cmd: str) -> bool:
47
+ data: dict[str, str] = cls.get_custom_prompt()
48
+ assert type_match(data, dict[str, str])
49
+
50
+ if name in data:
51
+ from .easyrip_log import log
52
+
53
+ log.error("The name {!r} is already in custom prompt", name)
54
+ return False
55
+
56
+ data[name] = cmd
57
+
58
+ cls.update_custom_prompt(data)
59
+
60
+ return True
61
+
62
+ @classmethod
63
+ def del_custom_prompt(cls, name: str) -> bool:
64
+ data: dict[str, str] = cls.get_custom_prompt()
65
+
66
+ pop_res: bool
67
+ if name in data:
68
+ data.pop(name)
69
+ pop_res = True
70
+ cls.update_custom_prompt(data)
71
+ else:
72
+ from .easyrip_log import log
73
+
74
+ log.warning("The name {!r} not in custom prompt", name)
75
+ pop_res = False
76
+
77
+ return pop_res
78
+
20
79
 
21
80
  class ConfigFileHistory(FileHistory):
22
81
  def store_string(self, string: str) -> None:
@@ -24,94 +83,95 @@ class ConfigFileHistory(FileHistory):
24
83
  super().store_string(string)
25
84
 
26
85
 
86
+ def _highlight_fuzzy_match(
87
+ suggestion: str,
88
+ user_input: str,
89
+ style_config: dict | None = None,
90
+ ) -> StyleAndTextTuples:
91
+ """
92
+ 高亮显示模糊匹配结果
93
+
94
+ Args:
95
+ suggestion: 建议的完整字符串
96
+ user_input: 用户输入的匹配字符
97
+ style_config: 样式配置字典
98
+
99
+ Returns:
100
+ 包含样式信息的格式化文本
101
+
102
+ """
103
+ if style_config is None:
104
+ style_config = {
105
+ "match_char": "class:fuzzymatch.inside.character",
106
+ "match_section": "class:fuzzymatch.inside",
107
+ "non_match": "class:fuzzymatch.outside",
108
+ }
109
+
110
+ if not user_input:
111
+ # 用户没有输入,返回原始字符串
112
+ return [(style_config["non_match"], suggestion)]
113
+
114
+ # 找到最佳匹配位置
115
+ result = []
116
+
117
+ # 简化的模糊匹配算法
118
+ pattern = ".*?".join(map(re.escape, user_input))
119
+ regex = re.compile(pattern, re.IGNORECASE)
120
+
121
+ match = regex.search(suggestion)
122
+ if not match:
123
+ # 没有匹配,返回原始字符串
124
+ return [(style_config["non_match"], suggestion)]
125
+
126
+ start, end = match.span()
127
+ match_text = suggestion[start:end]
128
+
129
+ # 匹配段之前的文本
130
+ if start > 0:
131
+ result.append((style_config["non_match"], suggestion[:start]))
132
+
133
+ # 匹配段内部的字符
134
+ input_chars = list(user_input)
135
+ for char in match_text:
136
+ if input_chars and char.lower() == input_chars[0].lower():
137
+ result.append((style_config["match_char"], char))
138
+ input_chars.pop(0)
139
+ else:
140
+ result.append((style_config["match_section"], char))
141
+
142
+ # 匹配段之后的文本
143
+ if end < len(suggestion):
144
+ result.append((style_config["non_match"], suggestion[end:]))
145
+
146
+ return result
147
+
148
+
149
+ def _fuzzy_filter_and_sort(names: list[str], match_str: str) -> list[str]:
150
+ """模糊过滤和排序"""
151
+ if not match_str:
152
+ return sorted(names)
153
+
154
+ # 构建模糊匹配模式
155
+ pattern = ".*?".join(map(re.escape, match_str))
156
+ regex = re.compile(f"(?=({pattern}))", re.IGNORECASE)
157
+
158
+ matches = []
159
+ for filename in names:
160
+ regex_matches = list(regex.finditer(filename))
161
+ if regex_matches:
162
+ # 找到最佳匹配(最左、最短)
163
+ best = min(regex_matches, key=lambda m: (m.start(), len(m.group(1))))
164
+ matches.append((best.start(), len(best.group(1)), filename))
165
+
166
+ # 按匹配质量排序:先按匹配位置,再按匹配长度
167
+ matches.sort(key=lambda x: (x[0], x[1]))
168
+ return [item[2] for item in matches]
169
+
170
+
27
171
  class SmartPathCompleter(Completer):
28
172
  def __init__(self) -> None:
29
173
  pass
30
174
 
31
- def _highlight_fuzzy_match(
32
- self,
33
- suggestion: str,
34
- user_input: str,
35
- style_config: dict | None = None,
36
- ) -> StyleAndTextTuples:
37
- """
38
- 高亮显示模糊匹配结果
39
-
40
- Args:
41
- suggestion: 建议的完整字符串
42
- user_input: 用户输入的匹配字符
43
- style_config: 样式配置字典
44
-
45
- Returns:
46
- 包含样式信息的格式化文本
47
-
48
- """
49
- if style_config is None:
50
- style_config = {
51
- "match_char": "class:fuzzymatch.inside.character",
52
- "match_section": "class:fuzzymatch.inside",
53
- "non_match": "class:fuzzymatch.outside",
54
- }
55
-
56
- if not user_input:
57
- # 用户没有输入,返回原始字符串
58
- return [(style_config["non_match"], suggestion)]
59
-
60
- # 找到最佳匹配位置
61
- result = []
62
-
63
- # 简化的模糊匹配算法
64
- pattern = ".*?".join(map(re.escape, user_input))
65
- regex = re.compile(pattern, re.IGNORECASE)
66
-
67
- match = regex.search(suggestion)
68
- if not match:
69
- # 没有匹配,返回原始字符串
70
- return [(style_config["non_match"], suggestion)]
71
-
72
- start, end = match.span()
73
- match_text = suggestion[start:end]
74
-
75
- # 匹配段之前的文本
76
- if start > 0:
77
- result.append((style_config["non_match"], suggestion[:start]))
78
-
79
- # 匹配段内部的字符
80
- input_chars = list(user_input)
81
- for char in match_text:
82
- if input_chars and char.lower() == input_chars[0].lower():
83
- result.append((style_config["match_char"], char))
84
- input_chars.pop(0)
85
- else:
86
- result.append((style_config["match_section"], char))
87
-
88
- # 匹配段之后的文本
89
- if end < len(suggestion):
90
- result.append((style_config["non_match"], suggestion[end:]))
91
-
92
- return result
93
-
94
- def _fuzzy_filter_and_sort(self, filenames: list[str], match_str: str) -> list[str]:
95
- """模糊过滤和排序"""
96
- if not match_str:
97
- return sorted(filenames)
98
-
99
- # 构建模糊匹配模式
100
- pattern = ".*?".join(map(re.escape, match_str))
101
- regex = re.compile(f"(?=({pattern}))", re.IGNORECASE)
102
-
103
- matches = []
104
- for filename in filenames:
105
- regex_matches = list(regex.finditer(filename))
106
- if regex_matches:
107
- # 找到最佳匹配(最左、最短)
108
- best = min(regex_matches, key=lambda m: (m.start(), len(m.group(1))))
109
- matches.append((best.start(), len(best.group(1)), filename))
110
-
111
- # 按匹配质量排序:先按匹配位置,再按匹配长度
112
- matches.sort(key=lambda x: (x[0], x[1]))
113
- return [item[2] for item in matches]
114
-
115
175
  def get_completions(
116
176
  self,
117
177
  document: Document,
@@ -128,7 +188,7 @@ class SmartPathCompleter(Completer):
128
188
  os.listdir(directory) if os.path.isdir(directory) else []
129
189
  )
130
190
 
131
- for filename in self._fuzzy_filter_and_sort(filenames, basename):
191
+ for filename in _fuzzy_filter_and_sort(filenames, basename):
132
192
  full_name = (
133
193
  filename if directory == "." else os.path.join(directory, filename)
134
194
  )
@@ -144,8 +204,29 @@ class SmartPathCompleter(Completer):
144
204
  yield Completion(
145
205
  text=completion,
146
206
  start_position=-len(text),
147
- display=self._highlight_fuzzy_match(filename, basename),
207
+ display=_highlight_fuzzy_match(filename, basename),
148
208
  )
149
209
 
150
210
  except OSError:
151
211
  pass
212
+
213
+
214
+ class CustomPromptCompleter(Completer):
215
+ def get_completions(
216
+ self,
217
+ document: Document,
218
+ complete_event: CompleteEvent, # noqa: ARG002
219
+ ) -> Iterable[Completion]:
220
+ text = document.text_before_cursor
221
+ words = text.split()
222
+
223
+ custom_prompt = easyrip_prompt.get_custom_prompt()
224
+ for word in words[-1:]:
225
+ for name in _fuzzy_filter_and_sort(list(custom_prompt), word):
226
+ target_cmd = custom_prompt[name]
227
+ yield Completion(
228
+ text=target_cmd,
229
+ start_position=-len(word),
230
+ display=_highlight_fuzzy_match(name, word),
231
+ display_meta=target_cmd,
232
+ )
easyrip/global_val.py CHANGED
@@ -4,7 +4,7 @@ from functools import cache
4
4
  from pathlib import Path
5
5
 
6
6
  PROJECT_NAME = "Easy Rip"
7
- PROJECT_VERSION = "4.13.2"
7
+ PROJECT_VERSION = "4.14.0"
8
8
  PROJECT_TITLE = f"{PROJECT_NAME} v{PROJECT_VERSION}"
9
9
  PROJECT_URL = "https://github.com/op200/EasyRip"
10
10
  PROJECT_RELEASE_API = "https://api.github.com/repos/op200/EasyRip/releases/latest"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: easyrip
3
- Version: 4.13.2
3
+ Version: 4.14.0
4
4
  Author: op200
5
5
  License-Expression: AGPL-3.0-or-later
6
6
  Project-URL: Homepage, https://github.com/op200/EasyRip
@@ -1,10 +1,10 @@
1
1
  easyrip/__init__.py,sha256=DULQoFEAEHYk7dS8Zxky56so7qDPqHm7jUc_Zop1eXw,616
2
- easyrip/__main__.py,sha256=wvMAp5i5-Pjv5dkEtPyoTs12QFq48B4u8lujYkcpy9I,5065
3
- easyrip/easyrip_command.py,sha256=AifME_dEPHzUAGJVFqwdIDE6RItOk2TO8QsKYV1kLYQ,29920
2
+ easyrip/__main__.py,sha256=AEYyrxi1iUR7nEU7zln8JEHmetjhLqOue4VhFZFR5gI,5158
3
+ easyrip/easyrip_command.py,sha256=1_CbICn_4U6xKEt0TCU753Oa2wC7_1966IoKNIdJoWI,30303
4
4
  easyrip/easyrip_log.py,sha256=R-dM3CWUBFITtG7GSD1zy4X4MhZqxkoiBPjlIpI76cY,15573
5
- easyrip/easyrip_main.py,sha256=LnMRaUC0qZm4cN5aSoUOVPZ6QvCqjRQZI9khzF5vf_s,46980
6
- easyrip/easyrip_prompt.py,sha256=3or0Vt4s6L53MCJtQmSylrTADZIIjX5gvpSb-JRe2P4,4844
7
- easyrip/global_val.py,sha256=_FS3SC-E8NBKHR2s_G8HYlqvrnzNkMdXwH7travwDWg,866
5
+ easyrip/easyrip_main.py,sha256=9KMpKUal8ApWZr80AQ7UwO1n6HUSWeVo2SuJ4pMW1f8,48130
6
+ easyrip/easyrip_prompt.py,sha256=TOmRJDigGRz7wRWFNakJdfNI1tn9vGekq6FH5ypmQfA,7068
7
+ easyrip/global_val.py,sha256=DRe2CSZP2HEBCcvEUVQRQLqR9_AfEQx-rQiYyjp7-NM,866
8
8
  easyrip/utils.py,sha256=N1rMF1MyoC-YFBgy10_u29cFoowfhR-5Viea93O7wQ4,8750
9
9
  easyrip/easyrip_config/config.py,sha256=KWXZMEYxdXYUGLQ-MR0A7nnOwR6QZdVrWBopfb2QZSA,9869
10
10
  easyrip/easyrip_config/config_key.py,sha256=_jjdKOunskUoG7UUWOz3QZK-s4LF_x6hmM9MKttyS2Q,766
@@ -24,9 +24,9 @@ easyrip/ripper/sub_and_font/__init__.py,sha256=cBT7mxL7RRFaJXFPXuZ7RT-YK6FbnanaU
24
24
  easyrip/ripper/sub_and_font/ass.py,sha256=EhDkVY5JXU77euWPId7H2v85j444m8ZLm7wUid7TYd8,35307
25
25
  easyrip/ripper/sub_and_font/font.py,sha256=X2dPcPzbwQf3fv_g_mxO-zY7puVAX9Nv-9QHn88q4oA,7745
26
26
  easyrip/ripper/sub_and_font/subset.py,sha256=--rAA3VH1rm_jBOC3yMs3rOJpn3tPuvfXqkimbBtx3s,18653
27
- easyrip-4.13.2.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
28
- easyrip-4.13.2.dist-info/METADATA,sha256=r-3nzElFwbocrzFqEbHh3r7Txzw31Fg5obONWANhlEs,3507
29
- easyrip-4.13.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
30
- easyrip-4.13.2.dist-info/entry_points.txt,sha256=D6GBMMTzZ-apgX76KyZ6jxMmIFqGYwU9neeLLni_qKI,49
31
- easyrip-4.13.2.dist-info/top_level.txt,sha256=kuEteBXm-Gf90jRQgH3-fTo-Z-Q6czSuUEqY158H4Ww,8
32
- easyrip-4.13.2.dist-info/RECORD,,
27
+ easyrip-4.14.0.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
28
+ easyrip-4.14.0.dist-info/METADATA,sha256=NiCk9-cdBcFFvcSrKArJmsZKCZheat4MnfPKmLkJ7zo,3507
29
+ easyrip-4.14.0.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
30
+ easyrip-4.14.0.dist-info/entry_points.txt,sha256=D6GBMMTzZ-apgX76KyZ6jxMmIFqGYwU9neeLLni_qKI,49
31
+ easyrip-4.14.0.dist-info/top_level.txt,sha256=kuEteBXm-Gf90jRQgH3-fTo-Z-Q6czSuUEqY158H4Ww,8
32
+ easyrip-4.14.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5