diff --git a/ui/curses_ui.py b/ui/curses_ui.py index dc4c969..7c35872 100644 --- a/ui/curses_ui.py +++ b/ui/curses_ui.py @@ -10,17 +10,104 @@ import ui.dialog import globals + +def handle_resize(stdscr, firstrun): + global messages_pad, messages_win, nodes_pad, nodes_win, channel_pad, channel_win, function_win, packetlog_win, entry_win + + # Calculate window max dimensions + height, width = stdscr.getmaxyx() + + # Define window dimensions and positions + channel_width = 3 * (width // 16) + nodes_width = 5 * (width // 16) + messages_width = width - channel_width - nodes_width + + if firstrun: + entry_win = curses.newwin(3, width, 0, 0) + channel_win = curses.newwin(height - 6, channel_width, 3, 0) + messages_win = curses.newwin(height - 6, messages_width, 3, channel_width) + nodes_win = curses.newwin(height - 6, nodes_width, 3, channel_width + messages_width) + function_win = curses.newwin(3, width, height - 3, 0) + packetlog_win = curses.newwin(int(height / 3), messages_width, height - int(height / 3) - 3, channel_width) + + # Will be resized to what we need when drawn + messages_pad = curses.newpad(1, 1) + nodes_pad = curses.newpad(1,1) + channel_pad = curses.newpad(1,1) + + entry_win.bkgd(get_color("background")) + channel_win.bkgd(get_color("background")) + messages_win.bkgd(get_color("background")) + nodes_win.bkgd(get_color("background")) + function_win.bkgd(get_color("background")) + packetlog_win.bkgd(get_color("background")) + + messages_pad.bkgd(get_color("background")) + nodes_pad.bkgd(get_color("background")) + channel_pad.bkgd(get_color("background")) + + + channel_win.attrset(get_color("window_frame")) + entry_win.attrset(get_color("window_frame")) + nodes_win.attrset(get_color("window_frame")) + messages_win.attrset(get_color("window_frame")) + function_win.attrset(get_color("window_frame")) + + else: + entry_win.erase() + channel_win.erase() + messages_win.erase() + nodes_win.erase() + function_win.erase() + packetlog_win.erase() + entry_win.resize(3, width) + channel_win.resize(height - 6, channel_width) + messages_win.resize(height - 6, messages_width) + messages_win.mvwin(3, channel_width) + nodes_win.resize(height - 6, nodes_width) + nodes_win.mvwin(3, channel_width + messages_width) + function_win.resize(3, width) + function_win.mvwin(height - 3, 0) + packetlog_win.resize(int(height / 3), messages_width) + packetlog_win.mvwin(height - int(height / 3) - 3, channel_width) + + + channel_win.box() + entry_win.box() + nodes_win.box() + messages_win.box() + + function_win.box() + + # Refresh all windows + entry_win.refresh() + channel_win.refresh() + function_win.refresh() + nodes_win.refresh() + messages_win.refresh() + + entry_win.keypad(True) + curses.curs_set(1) + + try: + draw_function_win() + draw_channel_list() + draw_messages_window(True) + draw_node_list() + except: + # Resize events can come faster than we can re-draw, which can cause a curses error. + # In this case we'll see another curses.KEY_RESIZE in our key handler and draw again later. + pass + + def main_ui(stdscr): - global messages_pad, messages_box, nodes_pad, nodes_box, channel_pad, channel_box, function_win, packetlog_win, entry_win - messages_pad = messages_box = nodes_pad = nodes_box = channel_pad = channel_box = function_win = packetlog_win = entry_win = None stdscr.keypad(True) get_channels() - - input_text = "" - + handle_resize(stdscr, True) + input_text = "" while True: draw_text_field(entry_win, f"Input: {input_text[-(stdscr.getmaxyx()[1] - 10):]}", get_color("input")) @@ -66,22 +153,22 @@ def main_ui(stdscr): elif char == curses.KEY_PPAGE: if globals.current_window == 0: - select_channel(globals.selected_channel - (channel_box.getmaxyx()[0] - 2)) # select_channel will bounds check for us + select_channel(globals.selected_channel - (channel_win.getmaxyx()[0] - 2)) # select_channel will bounds check for us elif globals.current_window == 1: globals.selected_message = max(globals.selected_message - get_msg_window_lines(), 0) refresh_pad(1) elif globals.current_window == 2: - select_node(globals.selected_node - (nodes_box.getmaxyx()[0] - 2)) # select_node will bounds check for us + select_node(globals.selected_node - (nodes_win.getmaxyx()[0] - 2)) # select_node will bounds check for us elif char == curses.KEY_NPAGE: if globals.current_window == 0: - select_channel(globals.selected_channel + (channel_box.getmaxyx()[0] - 2)) # select_channel will bounds check for us + select_channel(globals.selected_channel + (channel_win.getmaxyx()[0] - 2)) # select_channel will bounds check for us elif globals.current_window == 1: msg_line_count = messages_pad.getmaxyx()[0] globals.selected_message = min(globals.selected_message + get_msg_window_lines(), msg_line_count - get_msg_window_lines()) refresh_pad(1) elif globals.current_window == 2: - select_node(globals.selected_node + (nodes_box.getmaxyx()[0] - 2)) # select_node will bounds check for us + select_node(globals.selected_node + (nodes_win.getmaxyx()[0] - 2)) # select_node will bounds check for us elif char == curses.KEY_LEFT or char == curses.KEY_RIGHT: delta = -1 if char == curses.KEY_LEFT else 1 @@ -90,43 +177,43 @@ def main_ui(stdscr): globals.current_window = (globals.current_window + delta) % 3 if old_window == 0: - channel_box.attrset(get_color("window_frame")) - channel_box.box() - channel_box.refresh() + channel_win.attrset(get_color("window_frame")) + channel_win.box() + channel_win.refresh() highlight_line(False, 0, globals.selected_channel) refresh_pad(0) if old_window == 1: - messages_box.attrset(get_color("window_frame")) - messages_box.box() - messages_box.refresh() + messages_win.attrset(get_color("window_frame")) + messages_win.box() + messages_win.refresh() refresh_pad(1) elif old_window == 2: draw_function_win() - nodes_box.attrset(get_color("window_frame")) - nodes_box.box() - nodes_box.refresh() + nodes_win.attrset(get_color("window_frame")) + nodes_win.box() + nodes_win.refresh() highlight_line(False, 2, globals.selected_node) refresh_pad(2) if globals.current_window == 0: - channel_box.attrset(get_color("window_frame_selected")) - channel_box.box() - channel_box.attrset(get_color("window_frame")) - channel_box.refresh() + channel_win.attrset(get_color("window_frame_selected")) + channel_win.box() + channel_win.attrset(get_color("window_frame")) + channel_win.refresh() highlight_line(True, 0, globals.selected_channel) refresh_pad(0) elif globals.current_window == 1: - messages_box.attrset(get_color("window_frame_selected")) - messages_box.box() - messages_box.attrset(get_color("window_frame")) - messages_box.refresh() + messages_win.attrset(get_color("window_frame_selected")) + messages_win.box() + messages_win.attrset(get_color("window_frame")) + messages_win.refresh() refresh_pad(1) elif globals.current_window == 2: draw_function_win() - nodes_box.attrset(get_color("window_frame_selected")) - nodes_box.box() - nodes_box.attrset(get_color("window_frame")) - nodes_box.refresh() + nodes_win.attrset(get_color("window_frame_selected")) + nodes_win.box() + nodes_win.attrset(get_color("window_frame")) + nodes_win.refresh() highlight_line(True, 2, globals.selected_node) refresh_pad(2) @@ -230,143 +317,6 @@ def main_ui(stdscr): else: input_text += chr(char) - - - -def draw_node_details(): - node = None - try: - node = globals.interface.nodesByNum[globals.node_list[globals.selected_node]] - except KeyError: - return - - function_win.erase() - function_win.box() - - nodestr = "" - width = function_win.getmaxyx()[1] - - node_details_list = [f"{node['user']['longName']} " - if 'user' in node and 'longName' in node['user'] else "", - f"({node['user']['shortName']})" - if 'user' in node and 'shortName' in node['user'] else "", - f" | {node['user']['hwModel']}" - if 'user' in node and 'hwModel' in node['user'] else "", - f" | {node['user']['role']}" - if 'user' in node and 'role' in node['user'] else ""] - - if globals.node_list[globals.selected_node] == globals.myNodeNum: - node_details_list.extend([f" | Bat: {node['deviceMetrics']['batteryLevel']}% ({node['deviceMetrics']['voltage']}v)" - if 'deviceMetrics' in node - and 'batteryLevel' in node['deviceMetrics'] - and 'voltage' in node['deviceMetrics'] else "", - f" | Up: {get_readable_duration(node['deviceMetrics']['uptimeSeconds'])}" if 'deviceMetrics' in node - and 'uptimeSeconds' in node['deviceMetrics'] else "", - f" | ChUtil: {node['deviceMetrics']['channelUtilization']:.2f}%" if 'deviceMetrics' in node - and 'channelUtilization' in node['deviceMetrics'] else "", - f" | AirUtilTX: {node['deviceMetrics']['airUtilTx']:.2f}%" if 'deviceMetrics' in node - and 'airUtilTx' in node['deviceMetrics'] else "", - ]) - else: - node_details_list.extend([f" | {get_time_ago(node['lastHeard'])}" if ('lastHeard' in node and node['lastHeard']) else "", - f" | Hops: {node['hopsAway']}" if 'hopsAway' in node else "", - f" | SNR: {node['snr']}dB" - if ('snr' in node and 'hopsAway' in node and node['hopsAway'] == 0) - else "", - ]) - - for s in node_details_list: - if len(nodestr) + len(s) < width - 2: - nodestr = nodestr + s - - draw_centered_text_field(function_win, nodestr, 0, get_color("commands")) - -def draw_help(): - cmds = ["↑→↓← = Select", " ENTER = Send", " ` = Settings", " ^P = Packet Log", " ESC = Quit", " ^t = Traceroute", " ^d = Archive Chat"] - function_str = "" - for s in cmds: - if(len(function_str) + len(s) < function_win.getmaxyx()[1] - 2): - function_str += s - - draw_centered_text_field(function_win, function_str, 0, get_color("commands")) - -def draw_function_win(): - if(globals.current_window == 2): - draw_node_details() - else: - draw_help() - -def get_msg_window_lines(): - packetlog_height = packetlog_win.getmaxyx()[0] - 1 if globals.display_log else 0 - return messages_box.getmaxyx()[0] - 2 - packetlog_height - -def refresh_pad(window): - win_height = channel_box.getmaxyx()[0] - - selected_item = globals.selected_channel - pad = channel_pad - box = channel_box - lines = box.getmaxyx()[0] - 2 - start_index = max(0, selected_item - (win_height - 3)) # Leave room for borders - - if(window == 1): - pad = messages_pad - box = messages_box - lines = get_msg_window_lines() - selected_item = globals.selected_message - start_index = globals.selected_message - - if globals.display_log: - packetlog_win.box() - packetlog_win.refresh() - - if(window == 2): - pad = nodes_pad - box = nodes_box - lines = box.getmaxyx()[0] - 2 - selected_item = globals.selected_node - start_index = max(0, selected_item - (win_height - 3)) # Leave room for borders - - - pad.refresh(start_index, 0, - box.getbegyx()[0] + 1, box.getbegyx()[1] + 1, - box.getbegyx()[0] + lines, box.getbegyx()[1] + box.getmaxyx()[1] - 2) - -def highlight_line(highlight, window, line): - pad = nodes_pad - color = get_color("node_list") - select_len = nodes_box.getmaxyx()[1] - 2 - - if(window == 0): - pad = channel_pad - color = get_color("channel_selected" if (line == globals.selected_channel and highlight == False) else "channel_list") - select_len = channel_box.getmaxyx()[1] - 2 - - pad.chgat(line, 1, select_len, color | curses.A_REVERSE if highlight else color) - -def add_notification(channel_number): - if channel_number not in globals.notifications: - globals.notifications.append(channel_number) - -def remove_notification(channel_number): - if channel_number in globals.notifications: - globals.notifications.remove(channel_number) - -def draw_text_field(win, text, color): - win.border() - win.addstr(1, 1, text, color) - -def draw_centered_text_field(win, text, y_offset, color): - height, width = win.getmaxyx() - x = (width - len(text)) // 2 - y = (height // 2) + y_offset - win.addstr(y, x, text, color) - win.refresh() - -def draw_debug(value): - function_win.addstr(1, 1, f"debug: {value} ") - function_win.refresh() - def draw_splash(stdscr): setup_colors() curses.curs_set(0) @@ -396,10 +346,10 @@ def draw_splash(stdscr): def draw_channel_list(): channel_pad.erase() - win_height, win_width = channel_box.getmaxyx() + win_height, win_width = channel_win.getmaxyx() start_index = max(0, globals.selected_channel - (win_height - 3)) # Leave room for borders - channel_pad.resize(len(globals.all_messages), channel_box.getmaxyx()[1]) + channel_pad.resize(len(globals.all_messages), channel_win.getmaxyx()[1]) idx = 0 for channel in globals.channel_list: @@ -425,10 +375,10 @@ def draw_channel_list(): channel_pad.addstr(idx, 1, truncated_channel, color) idx += 1 - channel_box.attrset(get_color("window_frame_selected") if globals.current_window == 0 else get_color("window_frame")) - channel_box.box() - channel_box.attrset((get_color("window_frame"))) - channel_box.refresh() + channel_win.attrset(get_color("window_frame_selected") if globals.current_window == 0 else get_color("window_frame")) + channel_win.box() + channel_win.attrset((get_color("window_frame"))) + channel_win.refresh() refresh_pad(0) @@ -446,9 +396,9 @@ def draw_messages_window(scroll_to_bottom = False): row = 0 for (prefix, message) in messages: full_message = f"{prefix}{message}" - wrapped_lines = textwrap.wrap(full_message, messages_box.getmaxyx()[1] - 2) + wrapped_lines = textwrap.wrap(full_message, messages_win.getmaxyx()[1] - 2) msg_line_count += len(wrapped_lines) - messages_pad.resize(msg_line_count, messages_box.getmaxyx()[1]) + messages_pad.resize(msg_line_count, messages_win.getmaxyx()[1]) for line in wrapped_lines: if prefix.startswith("--"): @@ -461,10 +411,10 @@ def draw_messages_window(scroll_to_bottom = False): messages_pad.addstr(row, 1, line, color) row += 1 - messages_box.attrset(get_color("window_frame_selected") if globals.current_window == 1 else get_color("window_frame")) - messages_box.box() - messages_box.attrset(get_color("window_frame")) - messages_box.refresh() + messages_win.attrset(get_color("window_frame_selected") if globals.current_window == 1 else get_color("window_frame")) + messages_win.box() + messages_win.attrset(get_color("window_frame")) + messages_win.refresh() if(scroll_to_bottom): globals.selected_message = max(msg_line_count - get_msg_window_lines(), 0) @@ -477,7 +427,7 @@ def draw_messages_window(scroll_to_bottom = False): def draw_node_list(): nodes_pad.erase() - box_width = nodes_box.getmaxyx()[1] + box_width = nodes_win.getmaxyx()[1] nodes_pad.resize(len(globals.node_list) + 1, box_width) for i, node_num in enumerate(globals.node_list): @@ -486,10 +436,10 @@ def draw_node_list(): node_str = f"{'🔐' if secure else '🔓'} {get_name_from_database(node_num, 'long')}".ljust(box_width - 2)[:box_width - 2] nodes_pad.addstr(i, 1, node_str, get_color("node_list", reverse=globals.selected_node == i and globals.current_window == 2)) - nodes_box.attrset(get_color("window_frame_selected") if globals.current_window == 2 else get_color("window_frame")) - nodes_box.box() - nodes_box.attrset(get_color("window_frame")) - nodes_box.refresh() + nodes_win.attrset(get_color("window_frame_selected") if globals.current_window == 2 else get_color("window_frame")) + nodes_win.box() + nodes_win.attrset(get_color("window_frame")) + nodes_win.refresh() refresh_pad(2) @@ -632,90 +582,136 @@ def search(win): entry_win.erase() -def handle_resize(stdscr, firstrun): - global messages_pad, messages_box, nodes_pad, nodes_box, channel_pad, channel_box, function_win, packetlog_win, entry_win - - # Calculate window max dimensions - height, width = stdscr.getmaxyx() - - # Define window dimensions and positions - channel_width = 3 * (width // 16) - nodes_width = 5 * (width // 16) - messages_width = width - channel_width - nodes_width - - if firstrun: - entry_win = curses.newwin(3, width, 0, 0) - channel_box = curses.newwin(height - 6, channel_width, 3, 0) - messages_box = curses.newwin(height - 6, messages_width, 3, channel_width) - nodes_box = curses.newwin(height - 6, nodes_width, 3, channel_width + messages_width) - function_win = curses.newwin(3, width, height - 3, 0) - packetlog_win = curses.newwin(int(height / 3), messages_width, height - int(height / 3) - 3, channel_width) - - # Will be resized to what we need when drawn - messages_pad = curses.newpad(1, 1) - nodes_pad = curses.newpad(1,1) - channel_pad = curses.newpad(1,1) - - entry_win.bkgd(get_color("background")) - channel_box.bkgd(get_color("background")) - messages_box.bkgd(get_color("background")) - nodes_box.bkgd(get_color("background")) - - messages_pad.bkgd(get_color("background")) - nodes_pad.bkgd(get_color("background")) - channel_pad.bkgd(get_color("background")) - - function_win.bkgd(get_color("background")) - packetlog_win.bkgd(get_color("background")) - - channel_box.attrset(get_color("window_frame")) - entry_win.attrset(get_color("window_frame")) - nodes_box.attrset(get_color("window_frame")) - messages_box.attrset(get_color("window_frame")) - function_win.attrset(get_color("window_frame")) - - else: - entry_win.erase() - channel_box.erase() - messages_box.erase() - nodes_box.erase() - function_win.erase() - packetlog_win.erase() - entry_win.resize(3, width) - channel_box.resize(height - 6, channel_width) - messages_box.resize(height - 6, messages_width) - messages_box.mvwin(3, channel_width) - nodes_box.resize(height - 6, nodes_width) - nodes_box.mvwin(3, channel_width + messages_width) - function_win.resize(3, width) - function_win.mvwin(height - 3, 0) - packetlog_win.resize(int(height / 3), messages_width) - packetlog_win.mvwin(height - int(height / 3) - 3, channel_width) - - - channel_box.box() - entry_win.box() - nodes_box.box() - messages_box.box() +def draw_node_details(): + node = None + try: + node = globals.interface.nodesByNum[globals.node_list[globals.selected_node]] + except KeyError: + return + function_win.erase() function_win.box() - # Refresh all windows - entry_win.refresh() - channel_box.refresh() - function_win.refresh() - nodes_box.refresh() - messages_box.refresh() + nodestr = "" + width = function_win.getmaxyx()[1] - entry_win.keypad(True) - curses.curs_set(1) + node_details_list = [f"{node['user']['longName']} " + if 'user' in node and 'longName' in node['user'] else "", + f"({node['user']['shortName']})" + if 'user' in node and 'shortName' in node['user'] else "", + f" | {node['user']['hwModel']}" + if 'user' in node and 'hwModel' in node['user'] else "", + f" | {node['user']['role']}" + if 'user' in node and 'role' in node['user'] else ""] - try: - draw_function_win() - draw_channel_list() - draw_messages_window(True) - draw_node_list() - except: - # Resize events can come faster than we can re-draw, which can cause a curses error. - # In this case we'll see another curses.KEY_RESIZE in our key handler and draw again later. - pass + if globals.node_list[globals.selected_node] == globals.myNodeNum: + node_details_list.extend([f" | Bat: {node['deviceMetrics']['batteryLevel']}% ({node['deviceMetrics']['voltage']}v)" + if 'deviceMetrics' in node + and 'batteryLevel' in node['deviceMetrics'] + and 'voltage' in node['deviceMetrics'] else "", + f" | Up: {get_readable_duration(node['deviceMetrics']['uptimeSeconds'])}" if 'deviceMetrics' in node + and 'uptimeSeconds' in node['deviceMetrics'] else "", + f" | ChUtil: {node['deviceMetrics']['channelUtilization']:.2f}%" if 'deviceMetrics' in node + and 'channelUtilization' in node['deviceMetrics'] else "", + f" | AirUtilTX: {node['deviceMetrics']['airUtilTx']:.2f}%" if 'deviceMetrics' in node + and 'airUtilTx' in node['deviceMetrics'] else "", + ]) + else: + node_details_list.extend([f" | {get_time_ago(node['lastHeard'])}" if ('lastHeard' in node and node['lastHeard']) else "", + f" | Hops: {node['hopsAway']}" if 'hopsAway' in node else "", + f" | SNR: {node['snr']}dB" + if ('snr' in node and 'hopsAway' in node and node['hopsAway'] == 0) + else "", + ]) + + for s in node_details_list: + if len(nodestr) + len(s) < width - 2: + nodestr = nodestr + s + + draw_centered_text_field(function_win, nodestr, 0, get_color("commands")) + +def draw_help(): + cmds = ["↑→↓← = Select", " ENTER = Send", " ` = Settings", " ^P = Packet Log", " ESC = Quit", " ^t = Traceroute", " ^d = Archive Chat"] + function_str = "" + for s in cmds: + if(len(function_str) + len(s) < function_win.getmaxyx()[1] - 2): + function_str += s + + draw_centered_text_field(function_win, function_str, 0, get_color("commands")) + +def draw_function_win(): + if(globals.current_window == 2): + draw_node_details() + else: + draw_help() + +def get_msg_window_lines(): + packetlog_height = packetlog_win.getmaxyx()[0] - 1 if globals.display_log else 0 + return messages_win.getmaxyx()[0] - 2 - packetlog_height + +def refresh_pad(window): + win_height = channel_win.getmaxyx()[0] + + selected_item = globals.selected_channel + pad = channel_pad + box = channel_win + lines = box.getmaxyx()[0] - 2 + start_index = max(0, selected_item - (win_height - 3)) # Leave room for borders + + if(window == 1): + pad = messages_pad + box = messages_win + lines = get_msg_window_lines() + selected_item = globals.selected_message + start_index = globals.selected_message + + if globals.display_log: + packetlog_win.box() + packetlog_win.refresh() + + if(window == 2): + pad = nodes_pad + box = nodes_win + lines = box.getmaxyx()[0] - 2 + selected_item = globals.selected_node + start_index = max(0, selected_item - (win_height - 3)) # Leave room for borders + + + pad.refresh(start_index, 0, + box.getbegyx()[0] + 1, box.getbegyx()[1] + 1, + box.getbegyx()[0] + lines, box.getbegyx()[1] + box.getmaxyx()[1] - 2) + +def highlight_line(highlight, window, line): + pad = nodes_pad + color = get_color("node_list") + select_len = nodes_win.getmaxyx()[1] - 2 + + if(window == 0): + pad = channel_pad + color = get_color("channel_selected" if (line == globals.selected_channel and highlight == False) else "channel_list") + select_len = channel_win.getmaxyx()[1] - 2 + + pad.chgat(line, 1, select_len, color | curses.A_REVERSE if highlight else color) + +def add_notification(channel_number): + if channel_number not in globals.notifications: + globals.notifications.append(channel_number) + +def remove_notification(channel_number): + if channel_number in globals.notifications: + globals.notifications.remove(channel_number) + +def draw_text_field(win, text, color): + win.border() + win.addstr(1, 1, text, color) + +def draw_centered_text_field(win, text, y_offset, color): + height, width = win.getmaxyx() + x = (width - len(text)) // 2 + y = (height // 2) + y_offset + win.addstr(y, x, text, color) + win.refresh() + +def draw_debug(value): + function_win.addstr(1, 1, f"debug: {value} ") + function_win.refresh() \ No newline at end of file