forked from iarv/contact
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b05072786 | ||
|
|
4455781e6c | ||
|
|
0c8aaee415 | ||
|
|
b97d9f4649 | ||
|
|
4152fb6a21 | ||
|
|
384e36dac2 | ||
|
|
65bca84fe6 | ||
|
|
16fa2830fd | ||
|
|
c8f1da99e3 | ||
|
|
702250c329 | ||
|
|
6291082405 | ||
|
|
4fa5148664 | ||
|
|
d62ec09eea | ||
|
|
61026dcc73 | ||
|
|
1362d3a219 | ||
|
|
981d72e688 |
@@ -53,22 +53,25 @@ logging.basicConfig(
|
||||
|
||||
app_state.lock = threading.Lock()
|
||||
|
||||
|
||||
# ------------------------------------------------------------------------------
|
||||
# Main Program Logic
|
||||
# ------------------------------------------------------------------------------
|
||||
def prompt_region_if_unset(args: object) -> None:
|
||||
"""Prompt user to set region if it is unset."""
|
||||
confirmation = get_list_input("Your region is UNSET. Set it now?", "Yes", ["Yes", "No"])
|
||||
if confirmation == "Yes":
|
||||
set_region(interface_state.interface)
|
||||
interface_state.interface.close()
|
||||
interface_state.interface = initialize_interface(args)
|
||||
|
||||
|
||||
def initialize_globals(args) -> None:
|
||||
def initialize_globals(args: object) -> None:
|
||||
"""Initializes interface and shared globals."""
|
||||
interface_state.interface = initialize_interface(args)
|
||||
|
||||
# Prompt for region if unset
|
||||
if interface_state.interface.localNode.localConfig.lora.region == 0:
|
||||
confirmation = get_list_input("Your region is UNSET. Set it now?", "Yes", ["Yes", "No"])
|
||||
if confirmation == "Yes":
|
||||
set_region(interface_state.interface)
|
||||
interface_state.interface.close()
|
||||
interface_state.interface = initialize_interface(args)
|
||||
prompt_region_if_unset(args)
|
||||
|
||||
interface_state.myNodeNum = get_nodeNum()
|
||||
ui_state.channel_list = get_channels()
|
||||
|
||||
@@ -29,34 +29,36 @@ from contact.utilities.singleton import ui_state, interface_state, app_state
|
||||
def play_sound():
|
||||
try:
|
||||
system = platform.system()
|
||||
sound_path = ""
|
||||
executable = ""
|
||||
|
||||
if system == "Darwin": # macOS
|
||||
if system == "Darwin": #macOS
|
||||
sound_path = "/System/Library/Sounds/Ping.aiff"
|
||||
if os.path.exists(sound_path):
|
||||
subprocess.run(["afplay", sound_path], check=True)
|
||||
return
|
||||
else:
|
||||
logging.warning(f"macOS sound file not found: {sound_path}")
|
||||
|
||||
executable = "afplay"
|
||||
elif system == "Linux":
|
||||
sound_path = "/usr/share/sounds/freedesktop/stereo/complete.oga"
|
||||
if(shutil.which("paplay")):
|
||||
executable = "paplay"
|
||||
else:
|
||||
executable = "aplay"
|
||||
|
||||
if executable != "" and sound_path != "":
|
||||
if os.path.exists(sound_path):
|
||||
if shutil.which("paplay"):
|
||||
subprocess.run(["paplay", sound_path], check=True)
|
||||
return
|
||||
elif shutil.which("aplay"):
|
||||
subprocess.run(["aplay", sound_path], check=True)
|
||||
if shutil.which(executable):
|
||||
subprocess.run([executable, sound_path], check=True,
|
||||
stdout=subprocess.DEVNULL, stderr = subprocess.DEVNULL)
|
||||
return
|
||||
else:
|
||||
logging.warning("No sound player found (paplay/aplay)")
|
||||
logging.warning("No sound player found (afplay/paplay/aplay)")
|
||||
else:
|
||||
logging.warning(f"Linux sound file not found: {sound_path}")
|
||||
|
||||
logging.warning(f"Sound file not found: {sound_path}")
|
||||
except subprocess.CalledProcessError as e:
|
||||
logging.error(f"Sound playback failed: {e}")
|
||||
except Exception as e:
|
||||
logging.error(f"Unexpected error: {e}")
|
||||
|
||||
|
||||
|
||||
# Final fallback: terminal beep
|
||||
print("\a")
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import curses
|
||||
import textwrap
|
||||
import logging
|
||||
import traceback
|
||||
from typing import Union
|
||||
@@ -12,7 +11,7 @@ from contact.utilities.db_handler import get_name_from_database, update_node_inf
|
||||
from contact.utilities.input_handlers import get_list_input
|
||||
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
|
||||
from contact.ui.nav_utils import move_main_highlight, draw_main_arrows, get_msg_window_lines, wrap_text
|
||||
from contact.utilities.singleton import ui_state, interface_state
|
||||
|
||||
|
||||
@@ -109,7 +108,7 @@ def main_ui(stdscr: curses.window) -> None:
|
||||
handle_resize(stdscr, True)
|
||||
|
||||
while True:
|
||||
draw_text_field(entry_win, f"Input: {input_text[-(stdscr.getmaxyx()[1] - 10):]}", get_color("input"))
|
||||
draw_text_field(entry_win, f"Input: {(input_text or '')[-(stdscr.getmaxyx()[1] - 10):]}", get_color("input"))
|
||||
|
||||
# Get user input from entry window
|
||||
char = entry_win.get_wch()
|
||||
@@ -138,13 +137,13 @@ def main_ui(stdscr: curses.window) -> None:
|
||||
handle_leftright(char)
|
||||
|
||||
elif char in (chr(curses.KEY_ENTER), chr(10), chr(13)):
|
||||
handle_enter(input_text)
|
||||
input_text = handle_enter(input_text)
|
||||
|
||||
elif char == chr(20): # Ctrl + t for Traceroute
|
||||
handle_ctrl_t(stdscr)
|
||||
|
||||
elif char in (curses.KEY_BACKSPACE, chr(127)):
|
||||
handle_backspace(entry_win, input_text)
|
||||
input_text = handle_backspace(entry_win, input_text)
|
||||
|
||||
elif char == "`": # ` Launch the settings interface
|
||||
handle_backtick(stdscr)
|
||||
@@ -298,7 +297,7 @@ def handle_leftright(char: int) -> None:
|
||||
refresh_pad(2)
|
||||
|
||||
|
||||
def handle_enter(input_text: str) -> None:
|
||||
def handle_enter(input_text: str) -> str:
|
||||
"""Handle Enter key events to send messages or select channels."""
|
||||
if ui_state.current_window == 2:
|
||||
node_list = ui_state.node_list
|
||||
@@ -318,6 +317,7 @@ def handle_enter(input_text: str) -> None:
|
||||
draw_node_list()
|
||||
draw_channel_list()
|
||||
draw_messages_window(True)
|
||||
return input_text
|
||||
|
||||
elif len(input_text) > 0:
|
||||
# Enter key pressed, send user input as message
|
||||
@@ -325,8 +325,9 @@ def handle_enter(input_text: str) -> None:
|
||||
draw_messages_window(True)
|
||||
|
||||
# Clear entry window and reset input text
|
||||
input_text = ""
|
||||
entry_win.erase()
|
||||
return ""
|
||||
return input_text
|
||||
|
||||
|
||||
def handle_ctrl_t(stdscr: curses.window) -> None:
|
||||
@@ -342,7 +343,7 @@ def handle_ctrl_t(stdscr: curses.window) -> None:
|
||||
handle_resize(stdscr, False)
|
||||
|
||||
|
||||
def handle_backspace(entry_win: curses.window, input_text: str) -> None:
|
||||
def handle_backspace(entry_win: curses.window, input_text: str) -> str:
|
||||
"""Handle backspace key events to remove the last character from input text."""
|
||||
if input_text:
|
||||
input_text = input_text[:-1]
|
||||
@@ -351,6 +352,7 @@ def handle_backspace(entry_win: curses.window, input_text: str) -> None:
|
||||
entry_win.addch(" ") #
|
||||
entry_win.move(y, x - 1)
|
||||
entry_win.refresh()
|
||||
return input_text
|
||||
|
||||
|
||||
def handle_backtick(stdscr: curses.window) -> None:
|
||||
@@ -549,7 +551,7 @@ def draw_messages_window(scroll_to_bottom: bool = False) -> None:
|
||||
row = 0
|
||||
for prefix, message in messages:
|
||||
full_message = f"{prefix}{message}"
|
||||
wrapped_lines = textwrap.wrap(full_message, messages_win.getmaxyx()[1] - 2)
|
||||
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])
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import curses
|
||||
import re
|
||||
from unicodedata import east_asian_width
|
||||
|
||||
from contact.ui.colors import get_color
|
||||
from contact.utilities.control_utils import transform_menu_path
|
||||
from typing import Any, Optional, List, Dict
|
||||
@@ -293,9 +295,16 @@ def get_wrapped_help_text(
|
||||
|
||||
return wrapped_help
|
||||
|
||||
def text_width(text: str) -> int:
|
||||
return sum(2 if east_asian_width(c) in "FW" else 1 for c in text)
|
||||
|
||||
def wrap_text(text: str, wrap_width: int) -> List[str]:
|
||||
"""Wraps text while preserving spaces and breaking long words."""
|
||||
|
||||
whitespace = '\t\n\x0b\x0c\r '
|
||||
whitespace_trans = dict.fromkeys(map(ord, whitespace), ord(' '))
|
||||
text = text.translate(whitespace_trans)
|
||||
|
||||
words = re.findall(r"\S+|\s+", text) # Capture words and spaces separately
|
||||
wrapped_lines = []
|
||||
line_buffer = ""
|
||||
@@ -304,11 +313,11 @@ def wrap_text(text: str, wrap_width: int) -> List[str]:
|
||||
wrap_width -= margin
|
||||
|
||||
for word in words:
|
||||
word_length = len(word)
|
||||
word_length = text_width(word)
|
||||
|
||||
if word_length > wrap_width: # Break long words
|
||||
if line_buffer:
|
||||
wrapped_lines.append(line_buffer)
|
||||
wrapped_lines.append(line_buffer.strip())
|
||||
line_buffer = ""
|
||||
line_length = 0
|
||||
for i in range(0, word_length, wrap_width):
|
||||
@@ -316,7 +325,7 @@ def wrap_text(text: str, wrap_width: int) -> List[str]:
|
||||
continue
|
||||
|
||||
if line_length + word_length > wrap_width and word.strip():
|
||||
wrapped_lines.append(line_buffer)
|
||||
wrapped_lines.append(line_buffer.strip())
|
||||
line_buffer = ""
|
||||
line_length = 0
|
||||
|
||||
@@ -324,7 +333,7 @@ def wrap_text(text: str, wrap_width: int) -> List[str]:
|
||||
line_length += word_length
|
||||
|
||||
if line_buffer:
|
||||
wrapped_lines.append(line_buffer)
|
||||
wrapped_lines.append(line_buffer.strip())
|
||||
|
||||
return wrapped_lines
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "contact"
|
||||
version = "1.3.10"
|
||||
version = "1.3.14"
|
||||
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