sermo 0.3.0__tar.gz → 0.3.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sermo
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Command-line Sermo client
5
5
  Author: Sermo
6
6
  Project-URL: Homepage, https://sermo.jyonn.space
@@ -104,7 +104,7 @@ sermo tui
104
104
  - 直接输入文字,按 `Enter` 发送。
105
105
  - `F5` 手动刷新。
106
106
  - `Ctrl+U` 清空输入。
107
- - `Ctrl+Q` 退出。
107
+ - `Esc` 或 `F10` 退出;输入框为空时也可以按 `q` 退出。
108
108
 
109
109
  打开聊天:
110
110
 
@@ -84,7 +84,7 @@ sermo tui
84
84
  - 直接输入文字,按 `Enter` 发送。
85
85
  - `F5` 手动刷新。
86
86
  - `Ctrl+U` 清空输入。
87
- - `Ctrl+Q` 退出。
87
+ - `Esc` 或 `F10` 退出;输入框为空时也可以按 `q` 退出。
88
88
 
89
89
  打开聊天:
90
90
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sermo"
7
- version = "0.3.0"
7
+ version = "0.3.2"
8
8
  description = "Command-line Sermo client"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sermo
3
- Version: 0.3.0
3
+ Version: 0.3.2
4
4
  Summary: Command-line Sermo client
5
5
  Author: Sermo
6
6
  Project-URL: Homepage, https://sermo.jyonn.space
@@ -104,7 +104,7 @@ sermo tui
104
104
  - 直接输入文字,按 `Enter` 发送。
105
105
  - `F5` 手动刷新。
106
106
  - `Ctrl+U` 清空输入。
107
- - `Ctrl+Q` 退出。
107
+ - `Esc` 或 `F10` 退出;输入框为空时也可以按 `q` 退出。
108
108
 
109
109
  打开聊天:
110
110
 
@@ -1,14 +1,65 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import curses
4
- import textwrap
5
4
  import time
5
+ import unicodedata
6
6
  from typing import Any
7
7
 
8
8
  from .api import ApiClient, SermoApiError
9
9
  from .formatting import chat_title, format_time, message_text, user_name
10
10
 
11
11
 
12
+ def cell_width(char: str) -> int:
13
+ if not char:
14
+ return 0
15
+ if unicodedata.combining(char):
16
+ return 0
17
+ if unicodedata.east_asian_width(char) in {"F", "W"}:
18
+ return 2
19
+ return 1
20
+
21
+
22
+ def display_width(text: str) -> int:
23
+ return sum(cell_width(char) for char in str(text))
24
+
25
+
26
+ def clip_display(text: str, max_width: int) -> str:
27
+ if max_width <= 0:
28
+ return ""
29
+ used = 0
30
+ result: list[str] = []
31
+ for char in str(text).replace("\n", " "):
32
+ width = cell_width(char)
33
+ if used + width > max_width:
34
+ break
35
+ result.append(char)
36
+ used += width
37
+ return "".join(result)
38
+
39
+
40
+ def pad_display(text: str, width: int) -> str:
41
+ clipped = clip_display(text, width)
42
+ return clipped + " " * max(0, width - display_width(clipped))
43
+
44
+
45
+ def wrap_display(text: str, width: int) -> list[str]:
46
+ if width <= 0:
47
+ return [""]
48
+ lines: list[str] = []
49
+ current: list[str] = []
50
+ used = 0
51
+ for char in str(text).replace("\n", " "):
52
+ char_width = cell_width(char)
53
+ if used + char_width > width and current:
54
+ lines.append("".join(current))
55
+ current = []
56
+ used = 0
57
+ current.append(char)
58
+ used += char_width
59
+ lines.append("".join(current))
60
+ return lines or [""]
61
+
62
+
12
63
  class SermoTui:
13
64
  def __init__(self, client: ApiClient, *, refresh_interval: float = 5.0, limit: int = 40):
14
65
  self.client = client
@@ -170,7 +221,10 @@ class SermoTui:
170
221
  except curses.error:
171
222
  return
172
223
 
173
- if key in ("\x11", "\x03"):
224
+ if key in ("\x03", "\x1b", curses.KEY_F10):
225
+ self.running = False
226
+ return
227
+ if key in ("q", "Q") and not self.input_line:
174
228
  self.running = False
175
229
  return
176
230
  if key == curses.KEY_F5:
@@ -210,15 +264,15 @@ class SermoTui:
210
264
 
211
265
  title = f"Sermo TUI · {self.space.get('name') or 'Sermo'} @{self.space.get('slug') or '-'}"
212
266
  self._add(stdscr, 0, 0, title, curses.A_BOLD | curses.color_pair(1), width)
213
- self._add(stdscr, 0, max(0, width - len(self.status) - 1), self.status, curses.color_pair(5), width)
267
+ self._add(stdscr, 0, max(0, width - display_width(self.status) - 1), self.status, curses.color_pair(5), width)
214
268
  self._draw_separator(stdscr, 1, width)
215
269
  self._draw_chats(stdscr, 2, left_width, content_height)
216
270
  self._draw_vertical_separator(stdscr, 2, left_width, height - 3)
217
271
  self._draw_messages(stdscr, 2, main_x, width - main_x, content_height)
218
272
  self._draw_input(stdscr, input_y, main_x, width - main_x)
219
- help_text = "↑/↓ 切换会话 · Enter 发送 · F5 刷新 · Ctrl+U 清空输入 · Ctrl+Q 退出"
273
+ help_text = "↑/↓ 切换会话 · Enter 发送 · F5 刷新 · Ctrl+U 清空 · Esc/F10 退出 · 空输入 q 退出"
220
274
  self._add(stdscr, help_y, 0, help_text, curses.A_DIM, width)
221
- stdscr.move(input_y, min(width - 1, main_x + 2 + len(self.input_line)))
275
+ stdscr.move(input_y, min(width - 1, main_x + 2 + display_width(self.input_line)))
222
276
  stdscr.refresh()
223
277
 
224
278
  def _draw_chats(self, stdscr: Any, top: int, width: int, height: int) -> None:
@@ -271,10 +325,11 @@ class SermoTui:
271
325
  text = message_text(message)
272
326
  prefix = f"{format_time(message.get('created_at'))} {author}: "
273
327
  attrs = curses.color_pair(2) if is_self else curses.A_NORMAL
274
- wrapped = textwrap.wrap(text, width=max(8, wrap_width - len(prefix))) or [""]
328
+ prefix_width = display_width(prefix)
329
+ wrapped = wrap_display(text, width=max(8, wrap_width - prefix_width))
275
330
  lines.append((prefix + wrapped[0], attrs))
276
331
  for extra in wrapped[1:]:
277
- lines.append((" " * min(len(prefix), wrap_width - 1) + extra, attrs))
332
+ lines.append((" " * min(prefix_width, wrap_width - 1) + extra, attrs))
278
333
  return lines
279
334
 
280
335
  def _draw_input(self, stdscr: Any, y: int, x: int, width: int) -> None:
@@ -299,9 +354,9 @@ class SermoTui:
299
354
  max_width = max(0, min(width or max_x - x, max_x - x))
300
355
  if max_width <= 0:
301
356
  return
302
- safe = str(text).replace("\n", " ")[:max_width]
357
+ safe = pad_display(str(text), max_width)
303
358
  try:
304
- stdscr.addstr(y, x, safe.ljust(max_width), attrs)
359
+ stdscr.addstr(y, x, safe, attrs)
305
360
  except curses.error:
306
361
  pass
307
362
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes