Compare commits

...

6 Commits

Author SHA1 Message Date
pdxlocations
2634828edc Improve UI responsiveness by adjusting minimum column width and refining window border drawing logic 2026-02-21 20:57:34 -07:00
pdxlocations
5e108c5fe5 version bump 2026-02-12 07:45:41 -08:00
pdxlocations
edef37b116 IP bug fix 2026-02-12 07:38:43 -08:00
pdxlocations
e7e1bf7852 Merge pull request #246 from pdxlocations:display-ip
Display Human-Readable IP's and Actually Save Nested Configs
2026-02-11 21:57:54 -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
6 changed files with 70 additions and 20 deletions

View File

@@ -18,7 +18,7 @@ from contact.ui.nav_utils import move_main_highlight, draw_main_arrows, get_msg_
from contact.utilities.singleton import ui_state, interface_state, menu_state
MIN_COL = 1 # "effectively zero" without breaking curses
MIN_COL = 2 # keep hidden panes at least border-safe for curses
root_win = None
@@ -48,14 +48,18 @@ def compute_widths(total_w: int, focus: int):
# tiny terminals: allocate something, anything
return max(1, total_w), 0, 0
hidden_w = MIN_COL
if focus == 0:
return total_w - 2 * MIN_COL, MIN_COL, MIN_COL
return total_w - 2 * hidden_w, hidden_w, hidden_w
if focus == 1:
return MIN_COL, total_w - 2 * MIN_COL, MIN_COL
return MIN_COL, MIN_COL, total_w - 2 * MIN_COL
return hidden_w, total_w - 2 * hidden_w, hidden_w
return hidden_w, hidden_w, total_w - 2 * hidden_w
def paint_frame(win, selected: bool) -> None:
h, w = win.getmaxyx()
if h < 2 or w < 2:
return
win.attrset(get_color("window_frame_selected") if selected else get_color("window_frame"))
win.box()
win.attrset(get_color("window_frame"))
@@ -141,9 +145,22 @@ def handle_resize(stdscr: curses.window, firstrun: bool) -> None:
packetlog_win.resize(pkt_h, messages_width)
packetlog_win.mvwin(height - pkt_h - entry_height, channel_width)
# Draw window borders
for win in [channel_win, entry_win, nodes_win, messages_win]:
win.box()
# Draw only currently relevant borders in single-pane mode.
if ui_state.single_pane_mode:
target_wins = [entry_win]
if ui_state.current_window == 0:
target_wins.append(channel_win)
elif ui_state.current_window == 1:
target_wins.append(messages_win)
else:
target_wins.append(nodes_win)
else:
target_wins = [channel_win, entry_win, nodes_win, messages_win]
for win in target_wins:
h, w = win.getmaxyx()
if h >= 2 and w >= 2:
win.box()
win.refresh()
entry_win.keypad(True)

View File

@@ -1,5 +1,6 @@
import base64
import curses
import ipaddress
import logging
import os
import sys
@@ -122,6 +123,22 @@ 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(int(current_value).to_bytes(4, "little", signed=False))
)
except ipaddress.AddressValueError:
pass
elif isinstance(current_value, str) and current_value.isdigit():
try:
current_value = str(
ipaddress.IPv4Address(int(current_value).to_bytes(4, "little", signed=False))
)
except ipaddress.AddressValueError:
pass
display_option = f"{display_name}"[: w // 2 - 2]
display_value = f"{current_value}"[: w // 2 - 4]

View File

@@ -435,10 +435,16 @@ def draw_main_arrows(win: object, max_index: int, window: int, **kwargs) -> None
usable_height = height - 2
usable_width = width - 2
if usable_height <= 0 or usable_width <= 0:
return
if window == 1 and ui_state.display_log:
if log_height := kwargs.get("log_height"):
usable_height -= log_height - 1
if usable_height <= 0:
return
if usable_height < max_index:
if ui_state.start_index[window] > 0:
win.addstr(1, usable_width, "", get_color("settings_default"))

View File

@@ -462,7 +462,10 @@ from contact.utilities.singleton import menu_state # Ensure this is imported
def get_fixed32_input(current_value: int) -> int:
original_value = current_value
ip_string = str(ipaddress.IPv4Address(current_value))
try:
ip_string = str(ipaddress.IPv4Address(int(current_value).to_bytes(4, "little", signed=False)))
except Exception:
ip_string = str(ipaddress.IPv4Address(current_value))
height = 10
width = get_dialog_width()
start_y = max(0, (curses.LINES - height) // 2)
@@ -524,7 +527,7 @@ def get_fixed32_input(current_value: int) -> int:
if len(octets) == 4 and all(octet.isdigit() and 0 <= int(octet) <= 255 for octet in octets):
curses.noecho()
curses.curs_set(0)
return int(ipaddress.ip_address(user_input))
return int.from_bytes(ipaddress.IPv4Address(user_input).packed, "little", signed=False)
else:
fixed32_win.addstr(
7,
@@ -536,7 +539,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.13"
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"}