Compare commits

...

5 Commits

Author SHA1 Message Date
pdxlocations
ba713366bc bump version 2026-02-11 21:58:20 -08:00
pdxlocations
1c2384ea8d actually save nested configs 2026-02-11 21:56:45 -08:00
pdxlocations
4cda264746 Display IPs Correctly 2026-02-11 21:40:42 -08:00
pdxlocations
0005aaf438 Bump version to 1.4.13 in pyproject.toml 2026-01-24 00:08:59 -08:00
pdxlocations
f39a09646a fix No Help Available translation 2026-01-24 00:08:33 -08:00
8 changed files with 91 additions and 64 deletions

View File

@@ -1,5 +1,6 @@
import base64
import curses
import ipaddress
import logging
import os
import sys
@@ -8,7 +9,9 @@ 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.control_utils import transform_menu_path
from contact.utilities.i18n import t
from contact.utilities.ini_utils import parse_ini_file
from contact.utilities.input_handlers import (
get_repeated_input,
get_text_input,
@@ -16,7 +19,6 @@ from contact.utilities.input_handlers import (
get_list_input,
get_admin_key_input,
)
from contact.utilities.i18n import t
from contact.ui.colors import get_color
from contact.ui.dialog import dialog
from contact.ui.menus import generate_menu_from_protobuf
@@ -121,6 +123,18 @@ def display_menu() -> tuple[object, object]:
full_key = ".".join(transformed_path + [option])
display_name = field_mapping.get(full_key, option)
if full_key.startswith("config.network.ipv4_config.") and option in {"ip", "gateway", "subnet", "dns"}:
if isinstance(current_value, int):
try:
current_value = str(ipaddress.IPv4Address(current_value))
except ipaddress.AddressValueError:
pass
elif isinstance(current_value, str) and current_value.isdigit():
try:
current_value = str(ipaddress.IPv4Address(int(current_value)))
except ipaddress.AddressValueError:
pass
display_option = f"{display_name}"[: w // 2 - 2]
display_value = f"{current_value}"[: w // 2 - 4]

View File

@@ -6,7 +6,7 @@ from typing import Any, List, Dict, Optional
from contact.ui.colors import get_color, setup_colors, COLOR_MAP
import contact.ui.default_config as config
from contact.ui.nav_utils import move_highlight, draw_arrows, update_help_window
from contact.utilities.control_utils import parse_ini_file
from contact.utilities.ini_utils import parse_ini_file
from contact.utilities.input_handlers import get_list_input
from contact.utilities.i18n import t
from contact.utilities.singleton import menu_state

View File

@@ -1,55 +1,7 @@
from typing import Optional, Tuple, Dict, List
from typing import List
import re
def parse_ini_file(ini_file_path: str) -> Tuple[Dict[str, str], Dict[str, str]]:
"""Parses an INI file and returns a mapping of keys to human-readable names and help text."""
field_mapping: Dict[str, str] = {}
help_text: Dict[str, str] = {}
current_section: Optional[str] = None
with open(ini_file_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
# Skip empty lines and comments
if not line or line.startswith(";") or line.startswith("#"):
continue
# Handle sections like [config.device]
if line.startswith("[") and line.endswith("]"):
current_section = line[1:-1]
continue
# Parse lines like: key, "Human-readable name", "helptext"
parts = [p.strip().strip('"') for p in line.split(",", 2)]
if len(parts) >= 2:
key = parts[0]
# If key is 'title', map directly to the section
if key == "title":
full_key = current_section
else:
full_key = f"{current_section}.{key}" if current_section else key
# Use the provided human-readable name or fallback to key
human_readable_name = parts[1] if parts[1] else key
field_mapping[full_key] = human_readable_name
# Handle help text or default
help = parts[2] if len(parts) == 3 and parts[2] else "No help available."
help_text[full_key] = help
else:
# Handle cases with only the key present
full_key = f"{current_section}.{key}" if current_section else key
field_mapping[full_key] = key
help_text[full_key] = "No help available."
return field_mapping, help_text
def transform_menu_path(menu_path: List[str]) -> List[str]:
"""Applies path replacements and normalizes entries in the menu path."""
path_replacements = {"Radio Settings": "config", "Module Settings": "module"}

View File

@@ -1,7 +1,7 @@
from typing import Optional
import contact.ui.default_config as config
from contact.utilities.control_utils import parse_ini_file
from contact.utilities.ini_utils import parse_ini_file
_translations = {}
_language = None

View File

@@ -0,0 +1,54 @@
from typing import Optional, Tuple, Dict
from contact.utilities import i18n
def parse_ini_file(ini_file_path: str) -> Tuple[Dict[str, str], Dict[str, str]]:
"""Parses an INI file and returns a mapping of keys to human-readable names and help text."""
try:
default_help = i18n.t("ui.help.no_help", default="No help available.")
except Exception:
default_help = "No help available."
field_mapping: Dict[str, str] = {}
help_text: Dict[str, str] = {}
current_section: Optional[str] = None
with open(ini_file_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
# Skip empty lines and comments
if not line or line.startswith(";") or line.startswith("#"):
continue
# Handle sections like [config.device]
if line.startswith("[") and line.endswith("]"):
current_section = line[1:-1]
continue
# Parse lines like: key, "Human-readable name", "helptext"
parts = [p.strip().strip('"') for p in line.split(",", 2)]
if len(parts) >= 2:
key = parts[0]
# If key is 'title', map directly to the section
if key == "title":
full_key = current_section
else:
full_key = f"{current_section}.{key}" if current_section else key
# Use the provided human-readable name or fallback to key
human_readable_name = parts[1] if parts[1] else key
field_mapping[full_key] = human_readable_name
# Handle help text or default
help = parts[2] if len(parts) == 3 and parts[2] else default_help
help_text[full_key] = help
else:
# Handle cases with only the key present
full_key = f"{current_section}.{key}" if current_section else key
field_mapping[full_key] = key
help_text[full_key] = default_help
return field_mapping, help_text

View File

@@ -536,7 +536,7 @@ def get_fixed32_input(current_value: int) -> int:
curses.napms(1500)
user_input = ""
elif key in (curses.KEY_BACKSPACE, 127):
elif key in (curses.KEY_BACKSPACE, curses.KEY_DC, 127, 8, "\b", "\x7f"):
user_input = user_input[:-1]
else:

View File

@@ -112,16 +112,23 @@ def save_changes(interface, modified_settings, menu_state):
else:
config_category = None
# Resolve the target config container, including nested sub-messages (e.g., network.ipv4_config)
config_container = None
if hasattr(node.localConfig, config_category):
config_container = getattr(node.localConfig, config_category)
elif hasattr(node.moduleConfig, config_category):
config_container = getattr(node.moduleConfig, config_category)
else:
logging.warning(f"Config category '{config_category}' not found in config.")
return
if len(menu_state.menu_path) >= 4:
nested_key = menu_state.menu_path[3]
if hasattr(config_container, nested_key):
config_container = getattr(config_container, nested_key)
for config_item, new_value in modified_settings.items():
# Check if the category exists in localConfig
if hasattr(node.localConfig, config_category):
config_subcategory = getattr(node.localConfig, config_category)
# Check if the category exists in moduleConfig
elif hasattr(node.moduleConfig, config_category):
config_subcategory = getattr(node.moduleConfig, config_category)
else:
logging.warning(f"Config category '{config_category}' not found in config.")
continue
config_subcategory = config_container
# Check if the config_item exists in the subcategory
if hasattr(config_subcategory, config_item):

View File

@@ -1,6 +1,6 @@
[project]
name = "contact"
version = "1.4.12"
version = "1.4.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"}