mirror of
https://github.com/pdxlocations/contact.git
synced 2026-03-28 17:12:35 +01:00
Compare commits
10 Commits
queue
...
single-pan
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68578b534a | ||
|
|
27b0ca7482 | ||
|
|
d939561e2d | ||
|
|
9ab3c4dbf9 | ||
|
|
d79b751599 | ||
|
|
0a00e08431 | ||
|
|
f2627e0ed4 | ||
|
|
a3f0232641 | ||
|
|
3987c183d8 | ||
|
|
fcb1a42ef2 |
@@ -72,6 +72,7 @@ def initialize_globals() -> None:
|
||||
interface_state.myNodeNum = get_nodeNum()
|
||||
ui_state.channel_list = get_channels()
|
||||
ui_state.node_list = get_node_list()
|
||||
ui_state.single_pane_mode = config.single_pane_mode.lower() == "true"
|
||||
pub.subscribe(on_receive, "meshtastic.receive")
|
||||
|
||||
init_nodedb()
|
||||
|
||||
@@ -24,7 +24,7 @@ from contact.utilities.db_handler import (
|
||||
)
|
||||
import contact.ui.default_config as config
|
||||
|
||||
from contact.utilities.singleton import ui_state, interface_state, app_state
|
||||
from contact.utilities.singleton import ui_state, interface_state, app_state, menu_state
|
||||
|
||||
|
||||
def play_sound():
|
||||
@@ -84,6 +84,9 @@ def on_receive(packet: Dict[str, Any], interface: Any) -> None:
|
||||
|
||||
if ui_state.display_log:
|
||||
draw_packetlog_win()
|
||||
|
||||
if ui_state.current_window == 4:
|
||||
menu_state.need_redraw = True
|
||||
try:
|
||||
if "decoded" not in packet:
|
||||
return
|
||||
|
||||
@@ -15,6 +15,49 @@ 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
|
||||
|
||||
MIN_COL = 1 # "effectively zero" without breaking curses
|
||||
root_win = None # set in main_ui
|
||||
|
||||
|
||||
# Draw arrows for a specific window id (0=channel,1=messages,2=nodes).
|
||||
def draw_window_arrows(window_id: int) -> None:
|
||||
|
||||
if window_id == 0:
|
||||
draw_main_arrows(channel_win, len(ui_state.channel_list), window=0)
|
||||
channel_win.refresh()
|
||||
elif window_id == 1:
|
||||
msg_line_count = messages_pad.getmaxyx()[0]
|
||||
draw_main_arrows(
|
||||
messages_win,
|
||||
msg_line_count,
|
||||
window=1,
|
||||
log_height=packetlog_win.getmaxyx()[0],
|
||||
)
|
||||
messages_win.refresh()
|
||||
elif window_id == 2:
|
||||
draw_main_arrows(nodes_win, len(ui_state.node_list), window=2)
|
||||
nodes_win.refresh()
|
||||
|
||||
|
||||
def compute_widths(total_w: int, focus: int):
|
||||
# focus: 0=channel, 1=messages, 2=nodes
|
||||
if total_w < 3 * MIN_COL:
|
||||
# tiny terminals: allocate something, anything
|
||||
return max(1, total_w), 0, 0
|
||||
|
||||
if focus == 0:
|
||||
return total_w - 2 * MIN_COL, MIN_COL, MIN_COL
|
||||
if focus == 1:
|
||||
return MIN_COL, total_w - 2 * MIN_COL, MIN_COL
|
||||
return MIN_COL, MIN_COL, total_w - 2 * MIN_COL
|
||||
|
||||
|
||||
def paint_frame(win, selected: bool) -> None:
|
||||
win.attrset(get_color("window_frame_selected") if selected else get_color("window_frame"))
|
||||
win.box()
|
||||
win.attrset(get_color("window_frame"))
|
||||
win.refresh()
|
||||
|
||||
|
||||
def handle_resize(stdscr: curses.window, firstrun: bool) -> None:
|
||||
"""Handle terminal resize events and redraw the UI accordingly."""
|
||||
@@ -23,25 +66,43 @@ def handle_resize(stdscr: curses.window, firstrun: bool) -> None:
|
||||
# Calculate window max dimensions
|
||||
height, width = stdscr.getmaxyx()
|
||||
|
||||
# Define window dimensions and positions
|
||||
channel_width = int(config.channel_list_16ths) * (width // 16)
|
||||
nodes_width = int(config.node_list_16ths) * (width // 16)
|
||||
messages_width = width - channel_width - nodes_width
|
||||
if ui_state.single_pane_mode:
|
||||
channel_width, messages_width, nodes_width = compute_widths(width, ui_state.current_window)
|
||||
else:
|
||||
channel_width = int(config.channel_list_16ths) * (width // 16)
|
||||
nodes_width = int(config.node_list_16ths) * (width // 16)
|
||||
messages_width = width - channel_width - nodes_width
|
||||
|
||||
channel_width = max(MIN_COL, channel_width)
|
||||
messages_width = max(MIN_COL, messages_width)
|
||||
nodes_width = max(MIN_COL, nodes_width)
|
||||
|
||||
# Ensure the three widths sum exactly to the terminal width by adjusting the focused pane
|
||||
total = channel_width + messages_width + nodes_width
|
||||
if total != width:
|
||||
delta = total - width
|
||||
if ui_state.current_window == 0:
|
||||
channel_width = max(MIN_COL, channel_width - delta)
|
||||
elif ui_state.current_window == 1:
|
||||
messages_width = max(MIN_COL, messages_width - delta)
|
||||
else:
|
||||
nodes_width = max(MIN_COL, nodes_width - delta)
|
||||
|
||||
entry_height = 3
|
||||
function_height = 3
|
||||
y_pad = entry_height + function_height
|
||||
packet_log_height = int(height / 3)
|
||||
content_h = max(1, height - y_pad)
|
||||
pkt_h = max(1, int(height / 3))
|
||||
|
||||
if firstrun:
|
||||
entry_win = curses.newwin(entry_height, width, 0, 0)
|
||||
channel_win = curses.newwin(height - y_pad, channel_width, entry_height, 0)
|
||||
messages_win = curses.newwin(height - y_pad, messages_width, entry_height, channel_width)
|
||||
nodes_win = curses.newwin(height - y_pad, nodes_width, entry_height, channel_width + messages_width)
|
||||
|
||||
channel_win = curses.newwin(content_h, channel_width, entry_height, 0)
|
||||
messages_win = curses.newwin(content_h, messages_width, entry_height, channel_width)
|
||||
nodes_win = curses.newwin(content_h, nodes_width, entry_height, channel_width + messages_width)
|
||||
|
||||
function_win = curses.newwin(function_height, width, height - function_height, 0)
|
||||
packetlog_win = curses.newwin(
|
||||
packet_log_height, messages_width, height - packet_log_height - function_height, channel_width
|
||||
)
|
||||
packetlog_win = curses.newwin(pkt_h, messages_width, height - pkt_h - function_height, channel_width)
|
||||
|
||||
# Will be resized to what we need when drawn
|
||||
messages_pad = curses.newpad(1, 1)
|
||||
@@ -66,19 +127,19 @@ def handle_resize(stdscr: curses.window, firstrun: bool) -> None:
|
||||
|
||||
entry_win.resize(3, width)
|
||||
|
||||
channel_win.resize(height - y_pad, channel_width)
|
||||
channel_win.resize(content_h, channel_width)
|
||||
|
||||
messages_win.resize(height - y_pad, messages_width)
|
||||
messages_win.resize(content_h, messages_width)
|
||||
messages_win.mvwin(3, channel_width)
|
||||
|
||||
nodes_win.resize(height - y_pad, nodes_width)
|
||||
nodes_win.resize(content_h, nodes_width)
|
||||
nodes_win.mvwin(entry_height, channel_width + messages_width)
|
||||
|
||||
function_win.resize(3, width)
|
||||
function_win.mvwin(height - function_height, 0)
|
||||
|
||||
packetlog_win.resize(packet_log_height, messages_width)
|
||||
packetlog_win.mvwin(height - packet_log_height - function_height, channel_width)
|
||||
packetlog_win.resize(pkt_h, messages_width)
|
||||
packetlog_win.mvwin(height - pkt_h - function_height, channel_width)
|
||||
|
||||
# Draw window borders
|
||||
for win in [channel_win, entry_win, nodes_win, messages_win, function_win]:
|
||||
@@ -93,6 +154,7 @@ def handle_resize(stdscr: curses.window, firstrun: bool) -> None:
|
||||
draw_channel_list()
|
||||
draw_messages_window(True)
|
||||
draw_node_list()
|
||||
draw_window_arrows(ui_state.current_window)
|
||||
|
||||
except:
|
||||
# Resize events can come faster than we can re-draw, which can cause a curses error.
|
||||
@@ -103,6 +165,9 @@ def handle_resize(stdscr: curses.window, firstrun: bool) -> None:
|
||||
def main_ui(stdscr: curses.window) -> None:
|
||||
"""Main UI loop for the curses interface."""
|
||||
global input_text
|
||||
global root_win
|
||||
|
||||
root_win = stdscr
|
||||
input_text = ""
|
||||
stdscr.keypad(True)
|
||||
get_channels()
|
||||
@@ -209,6 +274,8 @@ def handle_home() -> None:
|
||||
elif ui_state.current_window == 2:
|
||||
select_node(0)
|
||||
|
||||
draw_window_arrows(ui_state.current_window)
|
||||
|
||||
|
||||
def handle_end() -> None:
|
||||
"""Handle end key events to select the last item in the current window."""
|
||||
@@ -220,29 +287,27 @@ def handle_end() -> None:
|
||||
refresh_pad(1)
|
||||
elif ui_state.current_window == 2:
|
||||
select_node(len(ui_state.node_list) - 1)
|
||||
draw_window_arrows(ui_state.current_window)
|
||||
|
||||
|
||||
def handle_pageup() -> None:
|
||||
"""Handle page up key events to scroll the current window by a page."""
|
||||
if ui_state.current_window == 0:
|
||||
select_channel(
|
||||
ui_state.selected_channel - (channel_win.getmaxyx()[0] - 2)
|
||||
) # select_channel will bounds check for us
|
||||
select_channel(ui_state.selected_channel - (channel_win.getmaxyx()[0] - 2))
|
||||
elif ui_state.current_window == 1:
|
||||
ui_state.selected_message = max(
|
||||
ui_state.selected_message - get_msg_window_lines(messages_win, packetlog_win), 0
|
||||
)
|
||||
refresh_pad(1)
|
||||
elif ui_state.current_window == 2:
|
||||
select_node(ui_state.selected_node - (nodes_win.getmaxyx()[0] - 2)) # select_node will bounds check for us
|
||||
select_node(ui_state.selected_node - (nodes_win.getmaxyx()[0] - 2))
|
||||
draw_window_arrows(ui_state.current_window)
|
||||
|
||||
|
||||
def handle_pagedown() -> None:
|
||||
"""Handle page down key events to scroll the current window down."""
|
||||
if ui_state.current_window == 0:
|
||||
select_channel(
|
||||
ui_state.selected_channel + (channel_win.getmaxyx()[0] - 2)
|
||||
) # select_channel will bounds check for us
|
||||
select_channel(ui_state.selected_channel + (channel_win.getmaxyx()[0] - 2))
|
||||
elif ui_state.current_window == 1:
|
||||
msg_line_count = messages_pad.getmaxyx()[0]
|
||||
ui_state.selected_message = min(
|
||||
@@ -251,7 +316,8 @@ def handle_pagedown() -> None:
|
||||
)
|
||||
refresh_pad(1)
|
||||
elif ui_state.current_window == 2:
|
||||
select_node(ui_state.selected_node + (nodes_win.getmaxyx()[0] - 2)) # select_node will bounds check for us
|
||||
select_node(ui_state.selected_node + (nodes_win.getmaxyx()[0] - 2))
|
||||
draw_window_arrows(ui_state.current_window)
|
||||
|
||||
|
||||
def handle_leftright(char: int) -> None:
|
||||
@@ -259,44 +325,36 @@ def handle_leftright(char: int) -> None:
|
||||
delta = -1 if char == curses.KEY_LEFT else 1
|
||||
old_window = ui_state.current_window
|
||||
ui_state.current_window = (ui_state.current_window + delta) % 3
|
||||
handle_resize(root_win, False)
|
||||
|
||||
if old_window == 0:
|
||||
channel_win.attrset(get_color("window_frame"))
|
||||
channel_win.box()
|
||||
channel_win.refresh()
|
||||
paint_frame(channel_win, selected=False)
|
||||
refresh_pad(0)
|
||||
if old_window == 1:
|
||||
messages_win.attrset(get_color("window_frame"))
|
||||
messages_win.box()
|
||||
messages_win.refresh()
|
||||
paint_frame(messages_win, selected=False)
|
||||
refresh_pad(1)
|
||||
elif old_window == 2:
|
||||
draw_function_win()
|
||||
nodes_win.attrset(get_color("window_frame"))
|
||||
nodes_win.box()
|
||||
nodes_win.refresh()
|
||||
paint_frame(nodes_win, selected=False)
|
||||
refresh_pad(2)
|
||||
|
||||
if not ui_state.single_pane_mode:
|
||||
draw_window_arrows(old_window)
|
||||
|
||||
if ui_state.current_window == 0:
|
||||
channel_win.attrset(get_color("window_frame_selected"))
|
||||
channel_win.box()
|
||||
channel_win.attrset(get_color("window_frame"))
|
||||
channel_win.refresh()
|
||||
paint_frame(channel_win, selected=True)
|
||||
refresh_pad(0)
|
||||
elif ui_state.current_window == 1:
|
||||
messages_win.attrset(get_color("window_frame_selected"))
|
||||
messages_win.box()
|
||||
messages_win.attrset(get_color("window_frame"))
|
||||
messages_win.refresh()
|
||||
paint_frame(messages_win, selected=True)
|
||||
refresh_pad(1)
|
||||
elif ui_state.current_window == 2:
|
||||
draw_function_win()
|
||||
nodes_win.attrset(get_color("window_frame_selected"))
|
||||
nodes_win.box()
|
||||
nodes_win.attrset(get_color("window_frame"))
|
||||
nodes_win.refresh()
|
||||
paint_frame(nodes_win, selected=True)
|
||||
refresh_pad(2)
|
||||
|
||||
# Draw arrows last; force even in multi-pane to avoid flicker
|
||||
draw_window_arrows(ui_state.current_window)
|
||||
|
||||
|
||||
def handle_enter(input_text: str) -> str:
|
||||
"""Handle Enter key events to send messages or select channels."""
|
||||
@@ -315,9 +373,11 @@ def handle_enter(input_text: str) -> str:
|
||||
ui_state.selected_node = 0
|
||||
ui_state.current_window = 0
|
||||
|
||||
handle_resize(root_win, False)
|
||||
draw_node_list()
|
||||
draw_channel_list()
|
||||
draw_messages_window(True)
|
||||
draw_window_arrows(ui_state.current_window)
|
||||
return input_text
|
||||
|
||||
elif len(input_text) > 0:
|
||||
@@ -330,8 +390,12 @@ def handle_enter(input_text: str) -> str:
|
||||
send_message(input_text, channel=ui_state.selected_channel)
|
||||
draw_messages_window(True)
|
||||
ui_state.last_sent_time = now
|
||||
# Clear entry window and reset input text
|
||||
entry_win.erase()
|
||||
|
||||
if ui_state.current_window == 0:
|
||||
ui_state.current_window = 1
|
||||
handle_resize(root_win, False)
|
||||
|
||||
return ""
|
||||
return input_text
|
||||
|
||||
@@ -499,6 +563,10 @@ def handle_ctlr_g(stdscr: curses.window) -> None:
|
||||
|
||||
def draw_channel_list() -> None:
|
||||
"""Update the channel list window and pad based on the current state."""
|
||||
|
||||
if ui_state.current_window != 0 and ui_state.single_pane_mode:
|
||||
return
|
||||
|
||||
channel_pad.erase()
|
||||
win_width = channel_win.getmaxyx()[1]
|
||||
|
||||
@@ -533,20 +601,18 @@ def draw_channel_list() -> None:
|
||||
channel_pad.addstr(idx, 1, truncated_channel, color)
|
||||
idx += 1
|
||||
|
||||
channel_win.attrset(
|
||||
get_color("window_frame_selected") if ui_state.current_window == 0 else get_color("window_frame")
|
||||
)
|
||||
channel_win.box()
|
||||
channel_win.attrset((get_color("window_frame")))
|
||||
|
||||
draw_main_arrows(channel_win, len(ui_state.channel_list), window=0)
|
||||
channel_win.refresh()
|
||||
|
||||
paint_frame(channel_win, selected=(ui_state.current_window == 0))
|
||||
refresh_pad(0)
|
||||
draw_window_arrows(0)
|
||||
channel_win.refresh()
|
||||
|
||||
|
||||
def draw_messages_window(scroll_to_bottom: bool = False) -> None:
|
||||
"""Update the messages window based on the selected channel and scroll position."""
|
||||
|
||||
if ui_state.current_window != 1 and ui_state.single_pane_mode:
|
||||
return
|
||||
|
||||
messages_pad.erase()
|
||||
|
||||
channel = ui_state.channel_list[ui_state.selected_channel]
|
||||
@@ -574,33 +640,21 @@ def draw_messages_window(scroll_to_bottom: bool = False) -> None:
|
||||
messages_pad.addstr(row, 1, line, color)
|
||||
row += 1
|
||||
|
||||
messages_win.attrset(
|
||||
get_color("window_frame_selected") if ui_state.current_window == 1 else get_color("window_frame")
|
||||
)
|
||||
messages_win.box()
|
||||
messages_win.attrset(get_color("window_frame"))
|
||||
messages_win.refresh()
|
||||
paint_frame(messages_win, selected=(ui_state.current_window == 1))
|
||||
|
||||
visible_lines = get_msg_window_lines(messages_win, packetlog_win)
|
||||
|
||||
if scroll_to_bottom:
|
||||
ui_state.selected_message = max(msg_line_count - visible_lines, 0)
|
||||
ui_state.start_index[1] = max(msg_line_count - visible_lines, 0)
|
||||
pass
|
||||
else:
|
||||
ui_state.selected_message = max(min(ui_state.selected_message, msg_line_count - visible_lines), 0)
|
||||
|
||||
draw_main_arrows(
|
||||
messages_win,
|
||||
msg_line_count,
|
||||
window=1,
|
||||
log_height=packetlog_win.getmaxyx()[0],
|
||||
)
|
||||
messages_win.refresh()
|
||||
|
||||
refresh_pad(1)
|
||||
|
||||
draw_packetlog_win()
|
||||
draw_window_arrows(1)
|
||||
messages_win.refresh()
|
||||
if ui_state.current_window == 4:
|
||||
menu_state.need_redraw = True
|
||||
|
||||
@@ -609,6 +663,9 @@ def draw_node_list() -> None:
|
||||
"""Update the nodes list window and pad based on the current state."""
|
||||
global nodes_pad
|
||||
|
||||
if ui_state.current_window != 2 and ui_state.single_pane_mode:
|
||||
return
|
||||
|
||||
# This didn't work, for some reason an error is thown on startup, so we just create the pad every time
|
||||
# if nodes_pad is None:
|
||||
# nodes_pad = curses.newpad(1, 1)
|
||||
@@ -637,16 +694,11 @@ def draw_node_list() -> None:
|
||||
i, 1, node_str, get_color(color, reverse=ui_state.selected_node == i and ui_state.current_window == 2)
|
||||
)
|
||||
|
||||
nodes_win.attrset(
|
||||
get_color("window_frame_selected") if ui_state.current_window == 2 else get_color("window_frame")
|
||||
)
|
||||
nodes_win.box()
|
||||
nodes_win.attrset(get_color("window_frame"))
|
||||
|
||||
draw_main_arrows(nodes_win, len(ui_state.node_list), window=2)
|
||||
paint_frame(nodes_win, selected=(ui_state.current_window == 2))
|
||||
nodes_win.refresh()
|
||||
|
||||
refresh_pad(2)
|
||||
draw_window_arrows(2)
|
||||
nodes_win.refresh()
|
||||
|
||||
# Restore cursor to input field
|
||||
entry_win.keypad(True)
|
||||
@@ -713,15 +765,9 @@ def scroll_messages(direction: int) -> None:
|
||||
0, min(ui_state.start_index[ui_state.current_window], max_index - visible_height + 1)
|
||||
)
|
||||
|
||||
draw_main_arrows(
|
||||
messages_win,
|
||||
msg_line_count,
|
||||
ui_state.current_window,
|
||||
log_height=packetlog_win.getmaxyx()[0],
|
||||
)
|
||||
messages_win.refresh()
|
||||
|
||||
refresh_pad(1)
|
||||
draw_window_arrows(ui_state.current_window)
|
||||
|
||||
|
||||
def select_node(idx: int) -> None:
|
||||
@@ -758,6 +804,9 @@ def draw_packetlog_win() -> None:
|
||||
columns = [10, 10, 15, 30]
|
||||
span = 0
|
||||
|
||||
if ui_state.current_window != 1 and ui_state.single_pane_mode:
|
||||
return
|
||||
|
||||
if ui_state.display_log:
|
||||
packetlog_win.erase()
|
||||
height, width = packetlog_win.getmaxyx()
|
||||
@@ -796,9 +845,7 @@ def draw_packetlog_win() -> None:
|
||||
# Add to the window
|
||||
packetlog_win.addstr(i + 2, 1, logString, get_color("log"))
|
||||
|
||||
packetlog_win.attrset(get_color("window_frame"))
|
||||
packetlog_win.box()
|
||||
packetlog_win.refresh()
|
||||
paint_frame(packetlog_win, selected=False)
|
||||
|
||||
# Restore cursor to input field
|
||||
entry_win.keypad(True)
|
||||
@@ -949,7 +996,7 @@ def draw_function_win() -> None:
|
||||
|
||||
|
||||
def refresh_pad(window: int) -> None:
|
||||
|
||||
# Derive the target box and pad for the requested window
|
||||
win_height = channel_win.getmaxyx()[0]
|
||||
|
||||
if window == 1:
|
||||
@@ -977,13 +1024,43 @@ def refresh_pad(window: int) -> None:
|
||||
selected_item = ui_state.selected_channel
|
||||
start_index = max(0, selected_item - (win_height - 3)) # Leave room for borders
|
||||
|
||||
# If in single-pane mode and this isn't the focused window, skip refreshing its (collapsed) pad
|
||||
if ui_state.single_pane_mode and window != ui_state.current_window:
|
||||
return
|
||||
|
||||
# Compute inner drawable area of the box
|
||||
box_y, box_x = box.getbegyx()
|
||||
box_h, box_w = box.getmaxyx()
|
||||
inner_h = max(0, box_h - 2) # minus borders
|
||||
inner_w = max(0, box_w - 2)
|
||||
|
||||
if inner_h <= 0 or inner_w <= 0:
|
||||
return
|
||||
|
||||
# Clamp lines to available inner height
|
||||
lines = max(0, min(lines, inner_h))
|
||||
|
||||
# Clamp start_index within the pad's height
|
||||
pad_h, pad_w = pad.getmaxyx()
|
||||
if pad_h <= 0:
|
||||
return
|
||||
start_index = max(0, min(start_index, max(0, pad_h - 1)))
|
||||
|
||||
top = box_y + 1
|
||||
left = box_x + 1
|
||||
bottom = box_y + min(inner_h, lines) # inclusive
|
||||
right = box_x + min(inner_w, box_w - 2)
|
||||
|
||||
if bottom < top or right < left:
|
||||
return
|
||||
|
||||
pad.refresh(
|
||||
start_index,
|
||||
0,
|
||||
box.getbegyx()[0] + 1,
|
||||
box.getbegyx()[1] + 1,
|
||||
box.getbegyx()[0] + lines,
|
||||
box.getbegyx()[1] + box.getmaxyx()[1] - 3,
|
||||
top,
|
||||
left,
|
||||
bottom,
|
||||
right,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -23,13 +23,22 @@ 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
|
||||
|
||||
# Constants
|
||||
width = 80
|
||||
# Setup Variables
|
||||
MAX_MENU_WIDTH = 80 # desired max; will shrink on small terminals
|
||||
save_option = "Save Changes"
|
||||
max_help_lines = 0
|
||||
help_win = None
|
||||
sensitive_settings = ["Reboot", "Reset Node DB", "Shutdown", "Factory Reset"]
|
||||
|
||||
|
||||
# Compute the effective menu width for the current terminal
|
||||
def get_menu_width() -> int:
|
||||
# Leave at least 2 columns for borders; clamp to >= 20 for usability
|
||||
return max(20, min(MAX_MENU_WIDTH, curses.COLS - 2))
|
||||
|
||||
|
||||
sensitive_settings = ["Reboot", "Reset Node DB", "Shutdown", "Factory Reset"]
|
||||
|
||||
# Get the parent directory of the script
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
parent_dir = os.path.abspath(os.path.join(script_dir, os.pardir))
|
||||
@@ -45,35 +54,39 @@ config_folder = os.path.abspath(config.node_configs_file_path)
|
||||
field_mapping, help_text = parse_ini_file(translation_file)
|
||||
|
||||
|
||||
def display_menu() -> tuple[object, object]: # curses.window or pad types
|
||||
def display_menu() -> tuple[object, object]:
|
||||
if help_win:
|
||||
min_help_window_height = 6
|
||||
else:
|
||||
min_help_window_height = 0
|
||||
|
||||
min_help_window_height = 6
|
||||
num_items = len(menu_state.current_menu) + (1 if menu_state.show_save_option else 0)
|
||||
|
||||
# Determine the available height for the menu
|
||||
max_menu_height = curses.LINES
|
||||
menu_height = min(max_menu_height - min_help_window_height, num_items + 5)
|
||||
w = get_menu_width()
|
||||
start_y = (curses.LINES - menu_height) // 2 - (min_help_window_height // 2)
|
||||
start_x = (curses.COLS - width) // 2
|
||||
start_x = (curses.COLS - w) // 2
|
||||
|
||||
# Calculate remaining space for help window
|
||||
global max_help_lines
|
||||
remaining_space = curses.LINES - (start_y + menu_height + 2) # +2 for padding
|
||||
max_help_lines = max(remaining_space, 1) # Ensure at least 1 lines for help
|
||||
|
||||
menu_win = curses.newwin(menu_height, width, start_y, start_x)
|
||||
menu_win = curses.newwin(menu_height, w, start_y, start_x)
|
||||
menu_win.erase()
|
||||
menu_win.bkgd(get_color("background"))
|
||||
menu_win.attrset(get_color("window_frame"))
|
||||
menu_win.border()
|
||||
menu_win.keypad(True)
|
||||
|
||||
menu_pad = curses.newpad(len(menu_state.current_menu) + 1, width - 8)
|
||||
menu_pad = curses.newpad(len(menu_state.current_menu) + 1, w - 8)
|
||||
menu_pad.bkgd(get_color("background"))
|
||||
|
||||
header = " > ".join(word.title() for word in menu_state.menu_path)
|
||||
if len(header) > width - 4:
|
||||
header = header[: width - 7] + "..."
|
||||
if len(header) > w - 4:
|
||||
header = header[: w - 7] + "..."
|
||||
menu_win.addstr(1, 2, header, get_color("settings_breadcrumbs", bold=True))
|
||||
|
||||
transformed_path = transform_menu_path(menu_state.menu_path)
|
||||
@@ -84,15 +97,15 @@ def display_menu() -> tuple[object, object]: # curses.window or pad types
|
||||
full_key = ".".join(transformed_path + [option])
|
||||
display_name = field_mapping.get(full_key, option)
|
||||
|
||||
display_option = f"{display_name}"[: width // 2 - 2]
|
||||
display_value = f"{current_value}"[: width // 2 - 4]
|
||||
display_option = f"{display_name}"[: w // 2 - 2]
|
||||
display_value = f"{current_value}"[: w // 2 - 4]
|
||||
|
||||
try:
|
||||
color = get_color(
|
||||
"settings_sensitive" if option in sensitive_settings else "settings_default",
|
||||
reverse=(idx == menu_state.selected_index),
|
||||
)
|
||||
menu_pad.addstr(idx, 0, f"{display_option:<{width // 2 - 2}} {display_value}".ljust(width - 8), color)
|
||||
menu_pad.addstr(idx, 0, f"{display_option:<{w // 2 - 2}} {display_value}".ljust(w - 8), color)
|
||||
except curses.error:
|
||||
pass
|
||||
|
||||
@@ -100,7 +113,7 @@ def display_menu() -> tuple[object, object]: # curses.window or pad types
|
||||
save_position = menu_height - 2
|
||||
menu_win.addstr(
|
||||
save_position,
|
||||
(width - len(save_option)) // 2,
|
||||
(w - len(save_option)) // 2,
|
||||
save_option,
|
||||
get_color("settings_save", reverse=(menu_state.selected_index == len(menu_state.current_menu))),
|
||||
)
|
||||
@@ -134,7 +147,6 @@ def draw_help_window(
|
||||
max_help_lines: int,
|
||||
transformed_path: List[str],
|
||||
) -> None:
|
||||
|
||||
global help_win
|
||||
|
||||
if "help_win" not in globals():
|
||||
@@ -145,8 +157,9 @@ def draw_help_window(
|
||||
)
|
||||
help_y = menu_start_y + menu_height
|
||||
|
||||
# Use current terminal width for the help window width calculation
|
||||
help_win = update_help_window(
|
||||
help_win, help_text, transformed_path, selected_option, max_help_lines, width, help_y, menu_start_x
|
||||
help_win, help_text, transformed_path, selected_option, max_help_lines, get_menu_width(), help_y, menu_start_x
|
||||
)
|
||||
|
||||
|
||||
@@ -231,10 +244,12 @@ def settings_menu(stdscr: object, interface: object) -> None:
|
||||
curses.update_lines_cols()
|
||||
|
||||
menu_win.erase()
|
||||
help_win.erase()
|
||||
if help_win:
|
||||
help_win.erase()
|
||||
|
||||
menu_win.refresh()
|
||||
help_win.refresh()
|
||||
if help_win:
|
||||
help_win.refresh()
|
||||
|
||||
elif key == ord("\t") and menu_state.show_save_option:
|
||||
old_selected_index = menu_state.selected_index
|
||||
@@ -254,12 +269,14 @@ def settings_menu(stdscr: object, interface: object) -> None:
|
||||
menu_state.need_redraw = True
|
||||
menu_state.start_index.append(0)
|
||||
menu_win.erase()
|
||||
help_win.erase()
|
||||
if help_win:
|
||||
help_win.erase()
|
||||
|
||||
# draw_help_window(menu_win.getbegyx()[0], menu_win.getbegyx()[1], menu_win.getmaxyx()[0], max_help_lines, menu_state.current_menu, selected_index, transform_menu_path(menu_state.menu_path))
|
||||
|
||||
menu_win.refresh()
|
||||
help_win.refresh()
|
||||
if help_win:
|
||||
help_win.refresh()
|
||||
|
||||
if menu_state.show_save_option and menu_state.selected_index == len(options):
|
||||
save_changes(interface, modified_settings, menu_state)
|
||||
@@ -538,13 +555,15 @@ def settings_menu(stdscr: object, interface: object) -> None:
|
||||
menu_state.need_redraw = True
|
||||
|
||||
menu_win.erase()
|
||||
help_win.erase()
|
||||
if help_win:
|
||||
help_win.erase()
|
||||
|
||||
# max_help_lines = 4
|
||||
# draw_help_window(menu_win.getbegyx()[0], menu_win.getbegyx()[1], menu_win.getmaxyx()[0], max_help_lines, menu_state.current_menu, selected_index, transform_menu_path(menu_state.menu_path))
|
||||
|
||||
menu_win.refresh()
|
||||
help_win.refresh()
|
||||
if help_win:
|
||||
help_win.refresh()
|
||||
|
||||
# if len(menu_state.menu_path) < 2:
|
||||
# modified_settings.clear()
|
||||
|
||||
@@ -183,6 +183,7 @@ def initialize_config() -> Dict[str, object]:
|
||||
default_config_variables = {
|
||||
"channel_list_16ths": "3",
|
||||
"node_list_16ths": "5",
|
||||
"single_pane_mode": "False",
|
||||
"db_file_path": db_file_path,
|
||||
"log_file_path": log_file_path,
|
||||
"node_configs_file_path": node_configs_file_path,
|
||||
@@ -228,12 +229,13 @@ def assign_config_variables(loaded_config: Dict[str, object]) -> None:
|
||||
|
||||
global db_file_path, log_file_path, node_configs_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 node_list_16ths, channel_list_16ths, single_pane_mode
|
||||
global theme, COLOR_CONFIG
|
||||
global node_sort, notification_sound
|
||||
|
||||
channel_list_16ths = loaded_config["channel_list_16ths"]
|
||||
node_list_16ths = loaded_config["node_list_16ths"]
|
||||
single_pane_mode = loaded_config["single_pane_mode"]
|
||||
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")
|
||||
|
||||
@@ -22,9 +22,9 @@ def get_node_color(node_index: int, reverse: bool = False):
|
||||
Segment = tuple[str, str, bool, bool]
|
||||
WrappedLine = List[Segment]
|
||||
|
||||
width = 80
|
||||
sensitive_settings = ["Reboot", "Reset Node DB", "Shutdown", "Factory Reset"]
|
||||
save_option = "Save Changes"
|
||||
MIN_HEIGHT_FOR_HELP = 20
|
||||
|
||||
|
||||
def move_highlight(
|
||||
@@ -73,9 +73,8 @@ def move_highlight(
|
||||
|
||||
# Clear old selection
|
||||
if show_save_option and old_idx == max_index:
|
||||
menu_win.chgat(
|
||||
menu_win.getmaxyx()[0] - 2, (width - len(save_option)) // 2, len(save_option), get_color("settings_save")
|
||||
)
|
||||
win_h, win_w = menu_win.getmaxyx()
|
||||
menu_win.chgat(win_h - 2, (win_w - len(save_option)) // 2, len(save_option), get_color("settings_save"))
|
||||
else:
|
||||
menu_pad.chgat(
|
||||
old_idx,
|
||||
@@ -90,9 +89,10 @@ def move_highlight(
|
||||
|
||||
# Highlight new selection
|
||||
if show_save_option and new_idx == max_index:
|
||||
win_h, win_w = menu_win.getmaxyx()
|
||||
menu_win.chgat(
|
||||
menu_win.getmaxyx()[0] - 2,
|
||||
(width - len(save_option)) // 2,
|
||||
win_h - 2,
|
||||
(win_w - len(save_option)) // 2,
|
||||
len(save_option),
|
||||
get_color("settings_save", reverse=True),
|
||||
)
|
||||
@@ -124,13 +124,14 @@ def move_highlight(
|
||||
selected_option = options[new_idx] if new_idx < len(options) else None
|
||||
help_y = menu_win.getbegyx()[0] + menu_win.getmaxyx()[0]
|
||||
if help_win:
|
||||
win_h, win_w = menu_win.getmaxyx()
|
||||
help_win = update_help_window(
|
||||
help_win,
|
||||
help_text,
|
||||
transformed_path,
|
||||
selected_option,
|
||||
max_help_lines,
|
||||
width,
|
||||
win_w,
|
||||
help_y,
|
||||
menu_win.getbegyx()[1],
|
||||
)
|
||||
@@ -167,23 +168,46 @@ def update_help_window(
|
||||
help_x: int,
|
||||
) -> object: # returns a curses window
|
||||
"""Handles rendering the help window consistently."""
|
||||
wrapped_help = get_wrapped_help_text(help_text, transformed_path, selected_option, width, max_help_lines)
|
||||
|
||||
if curses.LINES < MIN_HEIGHT_FOR_HELP:
|
||||
return None
|
||||
|
||||
# Clamp target position and width to the current terminal size
|
||||
help_x = max(0, help_x)
|
||||
help_y = max(0, help_y)
|
||||
# Ensure requested width fits on screen from help_x
|
||||
max_w_from_x = max(1, curses.COLS - help_x)
|
||||
safe_width = min(width, max_w_from_x)
|
||||
# Always leave a minimal border area; enforce a minimum usable width of 3
|
||||
safe_width = max(3, safe_width)
|
||||
|
||||
wrapped_help = get_wrapped_help_text(help_text, transformed_path, selected_option, safe_width, max_help_lines)
|
||||
|
||||
help_height = min(len(wrapped_help) + 2, max_help_lines + 2) # +2 for border
|
||||
help_height = max(help_height, 3) # Ensure at least 3 rows (1 text + border)
|
||||
|
||||
# Ensure help window does not exceed screen size
|
||||
# Re-clamp Y to keep the window visible
|
||||
if help_y + help_height > curses.LINES:
|
||||
help_y = curses.LINES - help_height
|
||||
help_y = max(0, curses.LINES - help_height)
|
||||
|
||||
# If width would overflow the screen, shrink it
|
||||
if help_x + safe_width > curses.COLS:
|
||||
safe_width = max(3, curses.COLS - help_x)
|
||||
|
||||
# Create or update the help window
|
||||
if help_win is None:
|
||||
help_win = curses.newwin(help_height, width, help_y, help_x)
|
||||
help_win = curses.newwin(help_height, safe_width, help_y, help_x)
|
||||
else:
|
||||
help_win.erase()
|
||||
help_win.refresh()
|
||||
help_win.resize(help_height, width)
|
||||
help_win.mvwin(help_y, help_x)
|
||||
help_win.resize(help_height, safe_width)
|
||||
try:
|
||||
help_win.mvwin(help_y, help_x)
|
||||
except curses.error:
|
||||
# If moving fails due to edge conditions, pin to (0,0) as a fallback
|
||||
help_y = 0
|
||||
help_x = 0
|
||||
help_win.mvwin(help_y, help_x)
|
||||
|
||||
help_win.bkgd(get_color("background"))
|
||||
help_win.attrset(get_color("window_frame"))
|
||||
@@ -295,14 +319,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(' '))
|
||||
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
|
||||
|
||||
@@ -31,6 +31,7 @@ class ChatUIState:
|
||||
start_index: List[int] = field(default_factory=lambda: [0, 0, 0])
|
||||
show_save_option: bool = False
|
||||
menu_path: List[str] = field(default_factory=list)
|
||||
single_pane_mode: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
|
||||
@@ -10,11 +10,17 @@ from contact.utilities.input_handlers import get_list_input
|
||||
from contact.utilities.singleton import menu_state
|
||||
|
||||
|
||||
width = 80
|
||||
MAX_MENU_WIDTH = 80 # desired max; will shrink on small terminals
|
||||
max_help_lines = 6
|
||||
save_option = "Save Changes"
|
||||
|
||||
|
||||
# Compute an effective width that fits the current terminal
|
||||
def get_effective_width() -> int:
|
||||
# Leave space for borders; ensure a sane minimum
|
||||
return max(20, min(MAX_MENU_WIDTH, curses.COLS - 2))
|
||||
|
||||
|
||||
def edit_color_pair(key: str, current_value: List[str]) -> List[str]:
|
||||
"""
|
||||
Allows the user to select a foreground and background color for a key.
|
||||
@@ -28,13 +34,14 @@ def edit_color_pair(key: str, current_value: List[str]) -> List[str]:
|
||||
|
||||
def edit_value(key: str, current_value: str) -> str:
|
||||
|
||||
w = get_effective_width()
|
||||
height = 10
|
||||
input_width = width - 16 # Allow space for "New Value: "
|
||||
input_width = w - 16 # Allow space for "New Value: "
|
||||
start_y = (curses.LINES - height) // 2
|
||||
start_x = (curses.COLS - width) // 2
|
||||
start_x = max(0, (curses.COLS - w) // 2)
|
||||
|
||||
# Create a centered window
|
||||
edit_win = curses.newwin(height, width, start_y, start_x)
|
||||
edit_win = curses.newwin(height, w, start_y, start_x)
|
||||
edit_win.bkgd(get_color("background"))
|
||||
edit_win.attrset(get_color("window_frame"))
|
||||
edit_win.border()
|
||||
@@ -43,7 +50,7 @@ def edit_value(key: str, current_value: str) -> str:
|
||||
edit_win.addstr(1, 2, f"Editing {key}", get_color("settings_default", bold=True))
|
||||
edit_win.addstr(3, 2, "Current Value:", get_color("settings_default"))
|
||||
|
||||
wrap_width = width - 4 # Account for border and padding
|
||||
wrap_width = w - 4 # Account for border and padding
|
||||
wrapped_lines = [current_value[i : i + wrap_width] for i in range(0, len(current_value), wrap_width)]
|
||||
|
||||
for i, line in enumerate(wrapped_lines[:4]): # Limit display to fit the window height
|
||||
@@ -67,6 +74,10 @@ def edit_value(key: str, current_value: str) -> str:
|
||||
sound_options = ["True", "False"]
|
||||
return get_list_input("Notification Sound", current_value, sound_options)
|
||||
|
||||
elif key == "single_pane_mode":
|
||||
sound_options = ["True", "False"]
|
||||
return get_list_input("Single-Pane Mode", current_value, sound_options)
|
||||
|
||||
# Standard Input Mode (Scrollable)
|
||||
edit_win.addstr(7, 2, "New Value: ", get_color("settings_default"))
|
||||
curses.curs_set(1)
|
||||
@@ -82,7 +93,7 @@ def edit_value(key: str, current_value: str) -> str:
|
||||
menu_state.need_redraw = False
|
||||
|
||||
# Re-create the window to fully reset state
|
||||
edit_win = curses.newwin(height, width, start_y, start_x)
|
||||
edit_win = curses.newwin(height, w, start_y, start_x)
|
||||
edit_win.timeout(200)
|
||||
edit_win.bkgd(get_color("background"))
|
||||
edit_win.attrset(get_color("window_frame"))
|
||||
@@ -150,11 +161,12 @@ def display_menu() -> tuple[Any, Any, List[str]]:
|
||||
max_menu_height = curses.LINES
|
||||
menu_height = min(max_menu_height, num_items + 5)
|
||||
num_items = len(options)
|
||||
w = get_effective_width()
|
||||
start_y = (curses.LINES - menu_height) // 2
|
||||
start_x = (curses.COLS - width) // 2
|
||||
start_x = max(0, (curses.COLS - w) // 2)
|
||||
|
||||
# Create the window
|
||||
menu_win = curses.newwin(menu_height, width, start_y, start_x)
|
||||
menu_win = curses.newwin(menu_height, w, start_y, start_x)
|
||||
menu_win.erase()
|
||||
menu_win.bkgd(get_color("background"))
|
||||
menu_win.attrset(get_color("window_frame"))
|
||||
@@ -162,13 +174,13 @@ def display_menu() -> tuple[Any, Any, List[str]]:
|
||||
menu_win.keypad(True)
|
||||
|
||||
# Create the pad for scrolling
|
||||
menu_pad = curses.newpad(num_items + 1, width - 8)
|
||||
menu_pad = curses.newpad(num_items + 1, w - 8)
|
||||
menu_pad.bkgd(get_color("background"))
|
||||
|
||||
# Display the menu path
|
||||
header = " > ".join(menu_state.menu_path)
|
||||
if len(header) > width - 4:
|
||||
header = header[: width - 7] + "..."
|
||||
if len(header) > w - 4:
|
||||
header = header[: w - 7] + "..."
|
||||
menu_win.addstr(1, 2, header, get_color("settings_breadcrumbs", bold=True))
|
||||
|
||||
# Populate the pad with menu options
|
||||
@@ -178,18 +190,18 @@ def display_menu() -> tuple[Any, Any, List[str]]:
|
||||
if isinstance(menu_state.current_menu, dict)
|
||||
else menu_state.current_menu[int(key.strip("[]"))]
|
||||
)
|
||||
display_key = f"{key}"[: width // 2 - 2]
|
||||
display_value = f"{value}"[: width // 2 - 8]
|
||||
display_key = f"{key}"[: w // 2 - 2]
|
||||
display_value = f"{value}"[: w // 2 - 8]
|
||||
|
||||
color = get_color("settings_default", reverse=(idx == menu_state.selected_index))
|
||||
menu_pad.addstr(idx, 0, f"{display_key:<{width // 2 - 2}} {display_value}".ljust(width - 8), color)
|
||||
menu_pad.addstr(idx, 0, f"{display_key:<{w // 2 - 2}} {display_value}".ljust(w - 8), color)
|
||||
|
||||
# Add Save button to the main window
|
||||
if menu_state.show_save_option:
|
||||
save_position = menu_height - 2
|
||||
menu_win.addstr(
|
||||
save_position,
|
||||
(width - len(save_option)) // 2,
|
||||
(w - len(save_option)) // 2,
|
||||
save_option,
|
||||
get_color("settings_save", reverse=(menu_state.selected_index == len(menu_state.current_menu))),
|
||||
)
|
||||
@@ -327,9 +339,10 @@ def json_editor(stdscr: curses.window, menu_state: Any) -> None:
|
||||
else:
|
||||
# Save button selected
|
||||
save_json(file_path, data)
|
||||
made_changes = False
|
||||
stdscr.refresh()
|
||||
# config.reload() # This isn't refreshing the file paths as expected
|
||||
continue
|
||||
break
|
||||
|
||||
elif key in (27, curses.KEY_LEFT): # Escape or Left Arrow
|
||||
|
||||
|
||||
@@ -4,6 +4,20 @@ import curses
|
||||
import ipaddress
|
||||
from typing import Any, Optional, List
|
||||
|
||||
# Dialogs should be at most 80 cols, but shrink on small terminals
|
||||
MAX_DIALOG_WIDTH = 80
|
||||
MIN_DIALOG_WIDTH = 20
|
||||
|
||||
|
||||
def get_dialog_width() -> int:
|
||||
# Leave 2 columns for borders and clamp to a sane minimum
|
||||
try:
|
||||
return max(MIN_DIALOG_WIDTH, min(MAX_DIALOG_WIDTH, curses.COLS - 2))
|
||||
except Exception:
|
||||
# Fallback if curses not ready yet
|
||||
return MAX_DIALOG_WIDTH
|
||||
|
||||
|
||||
from contact.ui.colors import get_color
|
||||
from contact.ui.nav_utils import move_highlight, draw_arrows, wrap_text
|
||||
from contact.ui.dialog import dialog
|
||||
@@ -45,13 +59,13 @@ def get_text_input(prompt: str, selected_config: str, input_type: str) -> Option
|
||||
input_win.refresh()
|
||||
|
||||
height = 8
|
||||
width = 80
|
||||
width = get_dialog_width()
|
||||
margin = 2 # Left and right margin
|
||||
input_width = width - (2 * margin) # Space available for text
|
||||
max_input_rows = height - 4 # Space for input
|
||||
|
||||
start_y = (curses.LINES - height) // 2
|
||||
start_x = (curses.COLS - width) // 2
|
||||
start_y = max(0, (curses.LINES - height) // 2)
|
||||
start_x = max(0, (curses.COLS - width) // 2)
|
||||
|
||||
input_win = curses.newwin(height, width, start_y, start_x)
|
||||
input_win.timeout(200)
|
||||
@@ -204,9 +218,7 @@ def get_text_input(prompt: str, selected_config: str, input_type: str) -> Option
|
||||
# Clear only the input area (without touching prompt text)
|
||||
for i in range(max_input_rows):
|
||||
if row + 1 + i < height - 1:
|
||||
input_win.addstr(
|
||||
row + 1 + i, margin, " " * min(input_width, width - margin - 1), get_color("settings_default")
|
||||
)
|
||||
input_win.addstr(row + 1 + i, margin, " " * input_width, get_color("settings_default"))
|
||||
|
||||
# Redraw the prompt text so it never disappears
|
||||
input_win.addstr(row + 1, margin, prompt_text, get_color("settings_default"))
|
||||
@@ -244,9 +256,9 @@ def get_admin_key_input(current_value: List[bytes]) -> Optional[List[str]]:
|
||||
|
||||
cvalue = to_base64(current_value) # Convert current values to Base64
|
||||
height = 9
|
||||
width = 80
|
||||
start_y = (curses.LINES - height) // 2
|
||||
start_x = (curses.COLS - width) // 2
|
||||
width = get_dialog_width()
|
||||
start_y = max(0, (curses.LINES - height) // 2)
|
||||
start_x = max(0, (curses.COLS - width) // 2)
|
||||
|
||||
admin_key_win = curses.newwin(height, width, start_y, start_x)
|
||||
admin_key_win.timeout(200)
|
||||
@@ -322,9 +334,9 @@ from contact.utilities.singleton import menu_state # Required if not already im
|
||||
|
||||
def get_repeated_input(current_value: List[str]) -> Optional[str]:
|
||||
height = 9
|
||||
width = 80
|
||||
start_y = (curses.LINES - height) // 2
|
||||
start_x = (curses.COLS - width) // 2
|
||||
width = get_dialog_width()
|
||||
start_y = max(0, (curses.LINES - height) // 2)
|
||||
start_x = max(0, (curses.COLS - width) // 2)
|
||||
|
||||
repeated_win = curses.newwin(height, width, start_y, start_x)
|
||||
repeated_win.timeout(200)
|
||||
@@ -344,15 +356,17 @@ def get_repeated_input(current_value: List[str]) -> Optional[str]:
|
||||
repeated_win.border()
|
||||
repeated_win.addstr(1, 2, "Edit up to 3 Values:", get_color("settings_default", bold=True))
|
||||
|
||||
win_h, win_w = repeated_win.getmaxyx()
|
||||
for i, line in enumerate(user_values):
|
||||
prefix = "→ " if i == cursor_pos else " "
|
||||
repeated_win.addstr(
|
||||
3 + i, 2, f"{prefix}Value{i + 1}: ", get_color("settings_default", bold=(i == cursor_pos))
|
||||
)
|
||||
repeated_win.addstr(3 + i, 18, line[: width - 20]) # Prevent overflow
|
||||
repeated_win.addstr(3 + i, 18, line[: max(0, win_w - 20)]) # Prevent overflow
|
||||
|
||||
if invalid_input:
|
||||
repeated_win.addstr(7, 2, invalid_input[: width - 4], get_color("settings_default", bold=True))
|
||||
win_h, win_w = repeated_win.getmaxyx()
|
||||
repeated_win.addstr(7, 2, invalid_input[: max(0, win_w - 4)], get_color("settings_default", bold=True))
|
||||
|
||||
repeated_win.move(3 + cursor_pos, 18 + len(user_values[cursor_pos]))
|
||||
repeated_win.refresh()
|
||||
@@ -404,9 +418,9 @@ def get_fixed32_input(current_value: int) -> int:
|
||||
original_value = current_value
|
||||
ip_string = str(ipaddress.IPv4Address(current_value))
|
||||
height = 10
|
||||
width = 80
|
||||
start_y = (curses.LINES - height) // 2
|
||||
start_x = (curses.COLS - width) // 2
|
||||
width = get_dialog_width()
|
||||
start_y = max(0, (curses.LINES - height) // 2)
|
||||
start_x = max(0, (curses.COLS - width) // 2)
|
||||
|
||||
fixed32_win = curses.newwin(height, width, start_y, start_x)
|
||||
fixed32_win.bkgd(get_color("background"))
|
||||
@@ -483,9 +497,9 @@ def get_list_input(
|
||||
selected_index = list_options.index(current_option) if current_option in list_options else 0
|
||||
|
||||
height = min(len(list_options) + 5, curses.LINES)
|
||||
width = 80
|
||||
start_y = (curses.LINES - height) // 2
|
||||
start_x = (curses.COLS - width) // 2
|
||||
width = get_dialog_width()
|
||||
start_y = max(0, (curses.LINES - height) // 2)
|
||||
start_x = max(0, (curses.COLS - width) // 2)
|
||||
|
||||
list_win = curses.newwin(height, width, start_y, start_x)
|
||||
list_win.timeout(200)
|
||||
@@ -493,7 +507,7 @@ def get_list_input(
|
||||
list_win.attrset(get_color("window_frame"))
|
||||
list_win.keypad(True)
|
||||
|
||||
list_pad = curses.newpad(len(list_options) + 1, width - 8)
|
||||
list_pad = curses.newpad(len(list_options) + 1, max(1, width - 8))
|
||||
list_pad.bkgd(get_color("background"))
|
||||
|
||||
max_index = len(list_options) - 1
|
||||
@@ -504,9 +518,11 @@ def get_list_input(
|
||||
list_win.border()
|
||||
list_win.addstr(1, 2, prompt, get_color("settings_default", bold=True))
|
||||
|
||||
win_h, win_w = list_win.getmaxyx()
|
||||
pad_w = max(1, win_w - 8)
|
||||
for idx, item in enumerate(list_options):
|
||||
color = get_color("settings_default", reverse=(idx == selected_index))
|
||||
list_pad.addstr(idx, 0, item.ljust(width - 8), color)
|
||||
list_pad.addstr(idx, 0, item[:pad_w].ljust(pad_w), color)
|
||||
|
||||
list_win.refresh()
|
||||
list_pad.refresh(
|
||||
@@ -517,7 +533,9 @@ def get_list_input(
|
||||
list_win.getbegyx()[0] + list_win.getmaxyx()[0] - 2,
|
||||
list_win.getbegyx()[1] + list_win.getmaxyx()[1] - 4,
|
||||
)
|
||||
draw_arrows(list_win, visible_height, max_index, [0], show_save_option=False)
|
||||
# Recompute visible height each draw in case of resize
|
||||
vis_h = list_win.getmaxyx()[0] - 5
|
||||
draw_arrows(list_win, vis_h, max_index, [0], show_save_option=False)
|
||||
|
||||
# Initial draw
|
||||
redraw_list_ui()
|
||||
|
||||
Reference in New Issue
Block a user