mirror of
https://github.com/pdxlocations/contact.git
synced 2026-03-28 17:12:35 +01:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a57823591a | ||
|
|
68b8d15181 | ||
|
|
4ef87871df | ||
|
|
18df7d326a | ||
|
|
c7b54caf45 | ||
|
|
773f43edd8 |
@@ -12,6 +12,7 @@ from contact.ui.colors import get_color
|
||||
from contact.utilities.db_handler import get_name_from_database, update_node_info_in_db, is_chat_archived
|
||||
from contact.utilities.input_handlers import get_list_input
|
||||
from contact.utilities.i18n import t
|
||||
from contact.utilities.emoji_utils import normalize_message_text
|
||||
import contact.ui.default_config as config
|
||||
import contact.ui.dialog
|
||||
from contact.ui.nav_utils import move_main_highlight, draw_main_arrows, get_msg_window_lines, wrap_text
|
||||
@@ -19,6 +20,7 @@ from contact.utilities.singleton import ui_state, interface_state, menu_state
|
||||
|
||||
|
||||
MIN_COL = 1 # "effectively zero" without breaking curses
|
||||
RESIZE_DEBOUNCE_MS = 250
|
||||
root_win = None
|
||||
|
||||
|
||||
@@ -161,6 +163,24 @@ def handle_resize(stdscr: curses.window, firstrun: bool) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def drain_resize_events(input_win: curses.window) -> Union[str, int, None]:
|
||||
"""Wait for resize events to settle and preserve one queued non-resize key."""
|
||||
input_win.timeout(RESIZE_DEBOUNCE_MS)
|
||||
try:
|
||||
while True:
|
||||
try:
|
||||
next_char = input_win.get_wch()
|
||||
except curses.error:
|
||||
return None
|
||||
|
||||
if next_char == curses.KEY_RESIZE:
|
||||
continue
|
||||
|
||||
return next_char
|
||||
finally:
|
||||
input_win.timeout(-1)
|
||||
|
||||
|
||||
def main_ui(stdscr: curses.window) -> None:
|
||||
"""Main UI loop for the curses interface."""
|
||||
global input_text
|
||||
@@ -168,6 +188,7 @@ def main_ui(stdscr: curses.window) -> None:
|
||||
|
||||
root_win = stdscr
|
||||
input_text = ""
|
||||
queued_char = None
|
||||
stdscr.keypad(True)
|
||||
get_channels()
|
||||
handle_resize(stdscr, True)
|
||||
@@ -176,7 +197,11 @@ def main_ui(stdscr: curses.window) -> None:
|
||||
draw_text_field(entry_win, f"Message: {(input_text or '')[-(stdscr.getmaxyx()[1] - 10):]}", get_color("input"))
|
||||
|
||||
# Get user input from entry window
|
||||
char = entry_win.get_wch()
|
||||
if queued_char is None:
|
||||
char = entry_win.get_wch()
|
||||
else:
|
||||
char = queued_char
|
||||
queued_char = None
|
||||
|
||||
# draw_debug(f"Keypress: {char}")
|
||||
|
||||
@@ -224,7 +249,9 @@ def main_ui(stdscr: curses.window) -> None:
|
||||
|
||||
elif char == curses.KEY_RESIZE:
|
||||
input_text = ""
|
||||
queued_char = drain_resize_events(entry_win)
|
||||
handle_resize(stdscr, False)
|
||||
continue
|
||||
|
||||
elif char == chr(4): # Ctrl + D to delete current channel or node
|
||||
handle_ctrl_d()
|
||||
@@ -839,7 +866,7 @@ def draw_messages_window(scroll_to_bottom: bool = False) -> None:
|
||||
|
||||
row = 0
|
||||
for prefix, message in messages:
|
||||
full_message = f"{prefix}{message}"
|
||||
full_message = normalize_message_text(f"{prefix}{message}")
|
||||
wrapped_lines = wrap_text(full_message, messages_win.getmaxyx()[1] - 2)
|
||||
msg_line_count += len(wrapped_lines)
|
||||
messages_pad.resize(msg_line_count, messages_win.getmaxyx()[1])
|
||||
|
||||
54
contact/utilities/emoji_utils.py
Normal file
54
contact/utilities/emoji_utils.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""Helpers for normalizing emoji sequences in width-sensitive message rendering."""
|
||||
|
||||
# Strip zero-width and presentation modifiers that make terminal cell width inconsistent.
|
||||
EMOJI_MODIFIER_REPLACEMENTS = {
|
||||
"\u200d": "",
|
||||
"\u20e3": "",
|
||||
"\ufe0e": "",
|
||||
"\ufe0f": "",
|
||||
"\U0001F3FB": "",
|
||||
"\U0001F3FC": "",
|
||||
"\U0001F3FD": "",
|
||||
"\U0001F3FE": "",
|
||||
"\U0001F3FF": "",
|
||||
}
|
||||
|
||||
_EMOJI_MODIFIER_TRANSLATION = str.maketrans(EMOJI_MODIFIER_REPLACEMENTS)
|
||||
_REGIONAL_INDICATOR_START = ord("\U0001F1E6")
|
||||
_REGIONAL_INDICATOR_END = ord("\U0001F1FF")
|
||||
|
||||
|
||||
def _regional_indicator_to_letter(char: str) -> str:
|
||||
return chr(ord("A") + ord(char) - _REGIONAL_INDICATOR_START)
|
||||
|
||||
|
||||
def _normalize_flag_emoji(text: str) -> str:
|
||||
"""Convert flag emoji built from regional indicators into ASCII country codes."""
|
||||
normalized = []
|
||||
index = 0
|
||||
|
||||
while index < len(text):
|
||||
current = text[index]
|
||||
current_ord = ord(current)
|
||||
|
||||
if _REGIONAL_INDICATOR_START <= current_ord <= _REGIONAL_INDICATOR_END and index + 1 < len(text):
|
||||
next_char = text[index + 1]
|
||||
next_ord = ord(next_char)
|
||||
if _REGIONAL_INDICATOR_START <= next_ord <= _REGIONAL_INDICATOR_END:
|
||||
normalized.append(_regional_indicator_to_letter(current))
|
||||
normalized.append(_regional_indicator_to_letter(next_char))
|
||||
index += 2
|
||||
continue
|
||||
|
||||
normalized.append(current)
|
||||
index += 1
|
||||
|
||||
return "".join(normalized)
|
||||
|
||||
|
||||
def normalize_message_text(text: str) -> str:
|
||||
"""Strip modifiers and rewrite flag emoji into stable terminal-friendly text."""
|
||||
if not text:
|
||||
return text
|
||||
|
||||
return _normalize_flag_emoji(text.translate(_EMOJI_MODIFIER_TRANSLATION))
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "contact"
|
||||
version = "1.4.17"
|
||||
version = "1.4.23"
|
||||
description = "This Python curses client for Meshtastic is a terminal-based client designed to manage device settings, enable mesh chat communication, and handle configuration backups and restores."
|
||||
authors = [
|
||||
{name = "Ben Lipsey",email = "ben@pdxlocations.com"}
|
||||
|
||||
Reference in New Issue
Block a user