Compare commits

..

3 Commits

Author SHA1 Message Date
pdxlocations
415d2bbda5 update lines and cols 2025-07-28 23:03:25 -07:00
pdxlocations
12d98ca999 add comment 2025-07-28 22:51:24 -07:00
pdxlocations
07f5889f74 Warn About 2-Second Message Delay 2025-07-28 22:36:29 -07:00
8 changed files with 66 additions and 111 deletions

View File

@@ -67,11 +67,3 @@ To quickly connect to localhost, use:
```sh
contact -t
```
## Install in development (editable) mode:
```bash
git clone https://github.com/pdxlocations/contact.git
cd contact
python3 -m venv .venv
source .venv/bin/activate
pip install -e .
```

View File

@@ -13,7 +13,7 @@ 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, wrap_text
from contact.utilities.singleton import ui_state, interface_state, menu_state
from contact.utilities.singleton import ui_state, interface_state
def handle_resize(stdscr: curses.window, firstrun: bool) -> None:
@@ -341,6 +341,7 @@ def handle_ctrl_t(stdscr: curses.window) -> None:
send_traceroute()
curses.curs_set(0) # Hide cursor
contact.ui.dialog.dialog(
stdscr,
f"Traceroute Sent To: {get_name_from_database(ui_state.node_list[ui_state.selected_node])}",
"Results will appear in messages window.\nNote: Traceroute is limited to once every 30 seconds.",
)
@@ -363,10 +364,7 @@ def handle_backspace(entry_win: curses.window, input_text: str) -> str:
def handle_backtick(stdscr: curses.window) -> None:
"""Handle backtick key events to open the settings menu."""
curses.curs_set(0)
previous_window = ui_state.current_window
ui_state.current_window = 4
settings_menu(stdscr, interface_state.interface)
ui_state.current_window = previous_window
curses.curs_set(1)
refresh_node_list()
handle_resize(stdscr, False)
@@ -601,8 +599,6 @@ def draw_messages_window(scroll_to_bottom: bool = False) -> None:
refresh_pad(1)
draw_packetlog_win()
if ui_state.current_window == 4:
menu_state.need_redraw = True
def draw_node_list() -> None:

View File

@@ -6,7 +6,6 @@ import sys
from typing import List
from contact.utilities.save_to_radio import save_changes
import contact.ui.default_config as config
from contact.utilities.config_io import config_export, config_import
from contact.utilities.control_utils import parse_ini_file, transform_menu_path
from contact.utilities.input_handlers import (
@@ -21,7 +20,9 @@ from contact.ui.dialog import dialog
from contact.ui.menus import generate_menu_from_protobuf
from contact.ui.nav_utils import move_highlight, draw_arrows, update_help_window
from contact.ui.user_config import json_editor
from contact.utilities.singleton import menu_state
from contact.ui.ui_state import MenuState
menu_state = MenuState()
# Constants
width = 80
@@ -35,19 +36,16 @@ script_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.abspath(os.path.join(script_dir, os.pardir))
# Paths
# locals_dir = os.path.dirname(os.path.abspath(sys.argv[0])) # Current script directory
locals_dir = os.path.dirname(os.path.abspath(sys.argv[0])) # Current script directory
translation_file = os.path.join(parent_dir, "localisations", "en.ini")
# config_folder = os.path.join(locals_dir, "node-configs")
config_folder = os.path.abspath(config.node_configs_file_path)
config_folder = os.path.join(locals_dir, "node-configs")
# Load translations
field_mapping, help_text = parse_ini_file(translation_file)
def display_menu() -> tuple[object, object]: # curses.window or pad types
def display_menu(menu_state: MenuState) -> tuple[object, object]: # curses.window or pad types
min_help_window_height = 6
num_items = len(menu_state.current_menu) + (1 if menu_state.show_save_option else 0)
@@ -108,7 +106,7 @@ def display_menu() -> tuple[object, object]: # curses.window or pad types
)
# Draw help window with dynamically updated max_help_lines
draw_help_window(start_y, start_x, menu_height, max_help_lines, transformed_path)
draw_help_window(start_y, start_x, menu_height, max_help_lines, transformed_path, menu_state)
menu_win.refresh()
menu_pad.refresh(
@@ -134,6 +132,7 @@ def draw_help_window(
menu_height: int,
max_help_lines: int,
transformed_path: List[str],
menu_state: MenuState,
) -> None:
global help_win
@@ -169,32 +168,29 @@ def settings_menu(stdscr: object, interface: object) -> None:
modified_settings = {}
menu_state.need_redraw = True
need_redraw = True
menu_state.show_save_option = False
while True:
if menu_state.need_redraw:
menu_state.need_redraw = False
if need_redraw:
options = list(menu_state.current_menu.keys())
# Determine if save option should be shown
path = menu_state.menu_path
menu_state.show_save_option = (
(len(path) > 2 and ("Radio Settings" in path or "Module Settings" in path))
or (len(path) == 2 and "User Settings" in path)
or (len(path) == 3 and "Channels" in path)
(
len(menu_state.menu_path) > 2
and ("Radio Settings" in menu_state.menu_path or "Module Settings" in menu_state.menu_path)
)
or (len(menu_state.menu_path) == 2 and "User Settings" in menu_state.menu_path)
or (len(menu_state.menu_path) == 3 and "Channels" in menu_state.menu_path)
)
# Display the menu
menu_win, menu_pad = display_menu()
menu_win, menu_pad = display_menu(menu_state)
if menu_win is None:
continue # Skip if menu_win is not initialized
need_redraw = False
menu_win.timeout(200) # wait up to 200 ms for a keypress (or less if key is pressed)
# Capture user input
key = menu_win.getch()
if key == -1:
continue
max_index = len(options) + (1 if menu_state.show_save_option else 0) - 1
# max_help_lines = 4
@@ -228,7 +224,7 @@ def settings_menu(stdscr: object, interface: object) -> None:
)
elif key == curses.KEY_RESIZE:
menu_state.need_redraw = True
need_redraw = True
curses.update_lines_cols()
menu_win.erase()
@@ -252,7 +248,7 @@ def settings_menu(stdscr: object, interface: object) -> None:
)
elif key == curses.KEY_RIGHT or key == ord("\n"):
menu_state.need_redraw = True
need_redraw = True
menu_state.start_index.append(0)
menu_win.erase()
help_win.erase()
@@ -394,7 +390,7 @@ def settings_menu(stdscr: object, interface: object) -> None:
menu_state.start_index.pop()
menu_state.selected_index = 4
continue
# menu_state.need_redraw = True
# need_redraw = True
field_info = menu_state.current_menu.get(selected_option)
if isinstance(field_info, tuple):
@@ -513,7 +509,7 @@ def settings_menu(stdscr: object, interface: object) -> None:
menu_state.selected_index = 0
elif key == curses.KEY_LEFT:
menu_state.need_redraw = True
need_redraw = True
menu_win.erase()
help_win.erase()

View File

@@ -2,7 +2,6 @@ import json
import logging
import os
from typing import Dict
from contact.ui.colors import setup_colors
# Get the parent directory of the script
script_dir = os.path.dirname(os.path.abspath(__file__))
@@ -14,12 +13,6 @@ parent_dir = os.path.abspath(os.path.join(script_dir, os.pardir))
# parent_dir = "/tmp/test_nonwritable"
def reload_config() -> None:
loaded_config = initialize_config()
assign_config_variables(loaded_config)
setup_colors(reinit=True)
def _is_writable_dir(path: str) -> bool:
"""
Return True if we can create & delete a temp file in `path`.
@@ -64,7 +57,6 @@ config_root = _get_config_root(parent_dir)
json_file_path = os.path.join(config_root, "config.json")
log_file_path = os.path.join(config_root, "client.log")
db_file_path = os.path.join(config_root, "client.db")
node_configs_file_path = os.path.join(config_root, "node-configs/")
def format_json_single_line_arrays(data: Dict[str, object], indent: int = 4) -> str:
@@ -185,7 +177,6 @@ def initialize_config() -> Dict[str, object]:
"node_list_16ths": "5",
"db_file_path": db_file_path,
"log_file_path": log_file_path,
"node_configs_file_path": node_configs_file_path,
"message_prefix": ">>",
"sent_message_prefix": ">> Sent",
"notification_symbol": "*",
@@ -226,7 +217,7 @@ def initialize_config() -> Dict[str, object]:
def assign_config_variables(loaded_config: Dict[str, object]) -> None:
# Assign values to local variables
global db_file_path, log_file_path, node_configs_file_path, message_prefix, sent_message_prefix
global db_file_path, log_file_path, message_prefix, sent_message_prefix
global notification_symbol, ack_implicit_str, ack_str, nak_str, ack_unknown_str
global node_list_16ths, channel_list_16ths
global theme, COLOR_CONFIG
@@ -236,7 +227,6 @@ def assign_config_variables(loaded_config: Dict[str, object]) -> None:
node_list_16ths = loaded_config["node_list_16ths"]
db_file_path = loaded_config["db_file_path"]
log_file_path = loaded_config["log_file_path"]
node_configs_file_path = loaded_config.get("node_configs_file_path")
message_prefix = loaded_config["message_prefix"]
sent_message_prefix = loaded_config["sent_message_prefix"]
notification_symbol = loaded_config["notification_symbol"]
@@ -268,7 +258,6 @@ if __name__ == "__main__":
print("\nLoaded Configuration:")
print(f"Database File Path: {db_file_path}")
print(f"Log File Path: {log_file_path}")
print(f"Configs File Path: {node_configs_file_path}")
print(f"Message Prefix: {message_prefix}")
print(f"Sent Message Prefix: {sent_message_prefix}")
print(f"Notification Symbol: {notification_symbol}")

View File

@@ -1,18 +1,14 @@
import curses
from contact.ui.colors import get_color
from contact.utilities.singleton import menu_state, ui_state
def dialog(title: str, message: str) -> None:
"""Display a dialog with a title and message."""
previous_window = ui_state.current_window
ui_state.current_window = 4
curses.update_lines_cols()
height, width = curses.LINES, curses.COLS
# Parse message into lines and calculate dimensions
# Calculate dialog dimensions
message_lines = message.splitlines()
max_line_length = max(len(l) for l in message_lines)
dialog_width = max(len(title) + 4, max_line_length + 4)
@@ -20,44 +16,36 @@ def dialog(title: str, message: str) -> None:
x = (width - dialog_width) // 2
y = (height - dialog_height) // 2
def draw_window():
win.erase()
win.bkgd(get_color("background"))
win.attrset(get_color("window_frame"))
win.border(0)
win.addstr(0, 2, title, get_color("settings_default"))
for i, line in enumerate(message_lines):
msg_x = (dialog_width - len(line)) // 2
win.addstr(2 + i, msg_x, line, get_color("settings_default"))
ok_text = " Ok "
win.addstr(
dialog_height - 2,
(dialog_width - len(ok_text)) // 2,
ok_text,
get_color("settings_default", reverse=True),
)
win.refresh()
# Create dialog window
win = curses.newwin(dialog_height, dialog_width, y, x)
draw_window()
win.bkgd(get_color("background"))
win.attrset(get_color("window_frame"))
win.border(0)
# Add title
win.addstr(0, 2, title, get_color("settings_default"))
# Add message (centered)
for i, line in enumerate(message_lines):
msg_x = (dialog_width - len(line)) // 2
win.addstr(2 + i, msg_x, line, get_color("settings_default"))
# Add centered OK button
ok_text = " Ok "
win.addstr(
dialog_height - 2,
(dialog_width - len(ok_text)) // 2,
ok_text,
get_color("settings_default", reverse=True),
)
# Refresh dialog window
win.refresh()
# Get user input
while True:
win.timeout(200)
char = win.getch()
if menu_state.need_redraw:
menu_state.need_redraw = False
draw_window()
if char in (curses.KEY_ENTER, 10, 13, 32, 27): # Enter, space, Esc
if char in (curses.KEY_ENTER, 10, 13, 32, 27): # Enter, space, or Esc
win.erase()
win.refresh()
ui_state.current_window = previous_window
return
if char == -1:
continue

View File

@@ -10,7 +10,6 @@ class MenuState:
current_menu: Union[Dict[str, Any], List[Any], str, int] = field(default_factory=dict)
menu_path: List[str] = field(default_factory=list)
show_save_option: bool = False
need_redraw: bool = False
@dataclass

View File

@@ -4,10 +4,9 @@ import curses
from typing import Any, List, Dict
from contact.ui.colors import get_color, setup_colors, COLOR_MAP
import contact.ui.default_config as config
from contact.ui.default_config import format_json_single_line_arrays, loaded_config
from contact.ui.nav_utils import move_highlight, draw_arrows
from contact.utilities.input_handlers import get_list_input
from contact.utilities.singleton import menu_state
width = 80
@@ -54,9 +53,7 @@ def edit_value(key: str, current_value: str) -> str:
# Handle theme selection dynamically
if key == "theme":
# Load theme names dynamically from the JSON
theme_options = [
k.split("_", 2)[2].lower() for k in config.loaded_config.keys() if k.startswith("COLOR_CONFIG")
]
theme_options = [k.split("_", 2)[2].lower() for k in loaded_config.keys() if k.startswith("COLOR_CONFIG")]
return get_list_input("Select Theme", current_value, theme_options)
elif key == "node_sort":
@@ -109,7 +106,7 @@ def edit_value(key: str, current_value: str) -> str:
return user_input if user_input else current_value
def display_menu() -> tuple[Any, Any, List[str]]:
def display_menu(menu_state: Any) -> tuple[Any, Any, List[str]]:
"""
Render the configuration menu with a Save button directly added to the window.
"""
@@ -214,14 +211,14 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None:
menu_state.current_menu = data # Track the current level of the menu
# Render the menu
menu_win, menu_pad, options = display_menu()
menu_state.need_redraw = True
menu_win, menu_pad, options = display_menu(menu_state)
need_redraw = True
while True:
if menu_state.need_redraw == True:
menu_win, menu_pad, options = display_menu()
if need_redraw:
menu_win, menu_pad, options = display_menu(menu_state)
menu_win.refresh()
menu_state.need_redraw = False
need_redraw = False
max_index = len(options) + (1 if menu_state.show_save_option else 0) - 1
key = menu_win.getch()
@@ -251,7 +248,7 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None:
elif key in (curses.KEY_RIGHT, 10, 13): # 10 = \n, 13 = carriage return
menu_state.need_redraw = True
need_redraw = True
menu_win.erase()
menu_win.refresh()
@@ -290,18 +287,17 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None:
menu_state.menu_index.pop()
menu_state.start_index.pop()
menu_state.current_menu[selected_key] = new_value
menu_state.need_redraw = True
need_redraw = True
else:
# Save button selected
save_json(file_path, data)
stdscr.refresh()
# config.reload() # This isn't refreshing the file paths as expected
continue
elif key in (27, curses.KEY_LEFT): # Escape or Left Arrow
menu_state.need_redraw = True
need_redraw = True
menu_win.erase()
menu_win.refresh()
@@ -329,7 +325,7 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None:
def save_json(file_path: str, data: Dict[str, Any]) -> None:
formatted_json = config.format_json_single_line_arrays(data)
formatted_json = format_json_single_line_arrays(data)
with open(file_path, "w", encoding="utf-8") as f:
f.write(formatted_json)
setup_colors(reinit=True)

View File

@@ -1,6 +1,5 @@
from contact.ui.ui_state import ChatUIState, InterfaceState, AppState, MenuState
from contact.ui.ui_state import ChatUIState, InterfaceState, AppState
ui_state = ChatUIState()
interface_state = InterfaceState()
app_state = AppState()
menu_state = MenuState()