Compare commits

...

57 Commits

Author SHA1 Message Date
pdxlocations
91d331af4f Merge pull request #60 from rfschmid/add-home-end-pgup-pgdn-to-channels-nodes 2025-01-23 16:15:43 -08:00
pdxlocations
be92ac5de3 Merge pull request #61 from rfschmid/box-active-window-in-green 2025-01-23 16:12:31 -08:00
Russell Schmidt
10179c4179 Add home/end/pgup/pgdn to channels and nodes 2025-01-23 17:38:43 -06:00
pdxlocations
6e45caaac2 clear menu_win on escape 2025-01-23 15:34:08 -08:00
Russell Schmidt
152555156e Outline active window in green 2025-01-23 17:28:12 -06:00
pdxlocations
056b8b5f5f Merge pull request #59 from pdxlocations/timestamps 2025-01-23 15:20:27 -08:00
pdxlocations
07f0721fd5 add hourly timestamp to rx and tx messages 2025-01-23 15:19:45 -08:00
pdxlocations
cf9276d399 add argparse to settings.py 2025-01-23 12:44:16 -08:00
pdxlocations
482d158b15 sort globals 2025-01-23 12:29:14 -08:00
pdxlocations
11bd9c75ed Add Hourly Timestamps (#58)
* init

* formatting

* white stamps and no seconds
2025-01-23 12:21:38 -08:00
pdxlocations
1b2701e1a1 white stamps and no seconds 2025-01-23 12:20:57 -08:00
pdxlocations
7fcc9abd76 formatting 2025-01-23 12:16:59 -08:00
pdxlocations
9bfddb954d init 2025-01-23 12:08:14 -08:00
pdxlocations
deb231fc63 Merge pull request #56 from rfschmid/stop-refreshing-when-changing-windows 2025-01-23 11:12:50 -08:00
pdxlocations
bbe9e66fa5 Merge pull request #54 from rfschmid:convert-channels-window-to-pad
Convert channels window to pad
2025-01-23 11:06:00 -08:00
Russell Schmidt
aa7d98b1b0 Stop refreshing when changing windows 2025-01-23 12:53:55 -06:00
Russell Schmidt
8895784503 Fix not clearing notifications 2025-01-23 12:29:32 -06:00
Russell Schmidt
695b4949c0 Fix drawing messages when switching channels 2025-01-23 12:09:21 -06:00
Russell Schmidt
404bac9133 Convert channels window to pad 2025-01-23 08:09:38 -06:00
pdxlocations
4698b81a3f Fix broken settings and simplify bool handlers (#53)
* underp modified settings and simplify bool  handling

* easier to read modified settings

* rm spaces
2025-01-22 22:21:52 -08:00
pdxlocations
e7e9f24fe2 Merge pull request #52 from pdxlocations/get-name-from-number-error
Don't iterate over the db while writing to it.
2025-01-22 20:56:12 -08:00
pdxlocations
01f67ea8b5 db snapshot 2025-01-22 20:54:51 -08:00
pdxlocations
c19684c119 Merge pull request #51 from rfschmid/convert-nodes-window-to-pad
Convert nodes window to pad
2025-01-22 20:43:44 -08:00
pdxlocations
0b0d8c482b Merge pull request #50 from rfschmid/add-home-end-page-support 2025-01-22 20:40:47 -08:00
Russell Schmidt
f9774b2248 Convert nodes window to pad 2025-01-22 21:39:19 -06:00
Russell Schmidt
d6db1e1832 Add home/end/pageup/pagedown message support 2025-01-22 20:48:14 -06:00
pdxlocations
4c85aaecdf sent messages cyan 2025-01-22 18:21:51 -08:00
pdxlocations
d8fc02b28a Merge pull request #36 from rfschmid/convert-messages-window-to-pad
Convert messages window to pad
2025-01-22 17:47:04 -08:00
Russell Schmidt
039673bb18 Disable message window looping 2025-01-22 19:41:30 -06:00
pdxlocations
d81e694ee6 Merge pull request #49 from pdxlocations/settings-cleanup
Add is_licensed to user settings
2025-01-22 17:32:38 -08:00
pdxlocations
dfea291d21 typoe 2025-01-22 17:31:33 -08:00
pdxlocations
41c60a49e9 Merge pull request #48 from rfschmid/fix-crash-on-long-input 2025-01-22 17:00:58 -08:00
pdxlocations
fb3138883f Merge pull request #47 from rfschmid/make-selected-channel-green 2025-01-22 16:54:56 -08:00
pdxlocations
767f0e2288 add is_licensed 2025-01-22 16:47:49 -08:00
Russell Schmidt
43680f8afb Fix crash on long input 2025-01-22 17:25:12 -06:00
Russell Schmidt
e15f625716 Fix default channel messages not showing on start 2025-01-22 17:14:49 -06:00
Russell Schmidt
c090b3dd58 Make messages pad actually work 2025-01-22 17:11:32 -06:00
pdxlocations
f472a3040c remove some unused code 2025-01-22 12:42:35 -08:00
pdxlocations
4a0c49b7d6 typo and cleanup 2025-01-22 12:18:58 -08:00
pdxlocations
3034a1464a ok to show duplicate messages from db 2025-01-22 11:59:06 -08:00
Russell Schmidt
80fe10c050 Make selected channel green
So you can tell which channel you're looking at when the channel list
isn't highlighted
2025-01-22 12:52:03 -06:00
pdxlocations
0962c5b284 break out of settings 2025-01-22 10:43:42 -08:00
pdxlocations
1b3abdebf2 some settings cleanup 2025-01-22 08:41:22 -08:00
pdxlocations
a710374fe9 refactor notification 2025-01-22 08:20:51 -08:00
Russell Schmidt
cb088c51d4 Count total lines of messages 2025-01-21 19:10:43 -06:00
Russell Schmidt
ccb46b8553 Account for packet log window 2025-01-21 19:10:33 -06:00
Russell Schmidt
35748d071e Various tweaks, added back messages border 2025-01-21 19:09:46 -06:00
Russell Schmidt
7e85085b98 WIP - Convert messages window to pad
Change to use a curses "pad" so we can scroll better
2025-01-21 19:09:44 -06:00
pdxlocations
7493d21c1a Merge pull request #45 from rfschmid/make-packet-log-ctrl-p 2025-01-21 15:54:50 -08:00
Russell Schmidt
d0af0e6af1 Make forward slash possible to type
Packet log is now Ctrl + P
2025-01-21 17:39:49 -06:00
pdxlocations
796c40b560 Merge pull request #43 from rfschmid/fix-wide-character-input
Fix wide character input
2025-01-21 15:14:59 -08:00
Russell Schmidt
1c0704b940 Handle backspace character
Co-authored-by: pdxlocations <117498748+pdxlocations@users.noreply.github.com>
2025-01-21 17:13:46 -06:00
Russell Schmidt
6384777bb6 Delete leftover comment 2025-01-21 14:58:34 -06:00
Russell Schmidt
2fbaee5fc5 Fix input of wide characters 2025-01-21 12:44:41 -06:00
pdxlocations
06e71331b6 select last message when changing channels 2025-01-21 09:54:33 -08:00
pdxlocations
fea705a09f Merge pull request #41 from pdxlocations/fix-repeated-input
ignore-incoming work - still not saving
2025-01-21 09:27:07 -08:00
pdxlocations
a47a4a9b32 ignore-incoming work - still not saving 2025-01-21 08:28:41 -08:00
11 changed files with 425 additions and 263 deletions

View File

@@ -1,6 +1,8 @@
import sqlite3
import globals
import time
from datetime import datetime
from utilities.utils import get_name_from_number
def get_table_name(channel):
@@ -69,6 +71,8 @@ def update_ack_nak(channel, timestamp, message, ack):
print(f"Unexpected error in update_ack_nak: {e}")
from datetime import datetime
def load_messages_from_db():
"""Load messages from the database for all channels and update globals.all_messages and globals.channel_list."""
try:
@@ -82,18 +86,18 @@ def load_messages_from_db():
# Iterate through each table and fetch its messages
for table_name in tables:
quoted_table_name = f'"{table_name}"' # Quote the table name becuase we begin with numerics and contain spaces
quoted_table_name = f'"{table_name}"' # Quote the table name because we begin with numerics and contain spaces
table_columns = [i[1] for i in db_cursor.execute(f'PRAGMA table_info({quoted_table_name})')]
if("ack_type" not in table_columns):
if "ack_type" not in table_columns:
update_table_query = f"ALTER TABLE {quoted_table_name} ADD COLUMN ack_type TEXT"
db_cursor.execute(update_table_query)
query = f'SELECT user_id, message_text, ack_type FROM {quoted_table_name}'
query = f'SELECT user_id, message_text, timestamp, ack_type FROM {quoted_table_name}'
try:
# Fetch all messages from the table
db_cursor.execute(query)
db_messages = [(row[0], row[1], row[2]) for row in db_cursor.fetchall()] # Save as tuples
db_messages = [(row[0], row[1], row[2], row[3]) for row in db_cursor.fetchall()] # Save as tuples
# Extract the channel name from the table name
channel = table_name.split("_")[1]
@@ -109,23 +113,32 @@ def load_messages_from_db():
if channel not in globals.all_messages:
globals.all_messages[channel] = []
# Add messages to globals.all_messages in tuple format
for user_id, message, ack_type in db_messages:
if user_id == str(globals.myNodeNum):
ack_str = globals.ack_unknown_str
if(ack_type == "Implicit"):
ack_str = globals.ack_implicit_str
elif(ack_type == "Ack"):
ack_str = globals.ack_str
elif(ack_type == "Nak"):
ack_str = globals.nak_str
# Add messages to globals.all_messages grouped by hourly timestamp
hourly_messages = {}
for user_id, message, timestamp, ack_type in db_messages:
hour = datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d %H:00')
if hour not in hourly_messages:
hourly_messages[hour] = []
ack_str = globals.ack_unknown_str
if ack_type == "Implicit":
ack_str = globals.ack_implicit_str
elif ack_type == "Ack":
ack_str = globals.ack_str
elif ack_type == "Nak":
ack_str = globals.nak_str
if user_id == str(globals.myNodeNum):
formatted_message = (f"{globals.sent_message_prefix}{ack_str}: ", message)
else:
else:
formatted_message = (f"{globals.message_prefix} {get_name_from_number(int(user_id), 'short')}: ", message)
if formatted_message not in globals.all_messages[channel]:
globals.all_messages[channel].append(formatted_message)
hourly_messages[hour].append(formatted_message)
# Flatten the hourly messages into globals.all_messages[channel]
for hour, messages in sorted(hourly_messages.items()):
globals.all_messages[channel].append((f"-- {hour} --", ""))
globals.all_messages[channel].extend(messages)
except sqlite3.Error as e:
print(f"SQLite error while loading messages from table '{table_name}': {e}")

View File

@@ -1,25 +1,27 @@
import os
# App Variables
app_directory = os.path.dirname(os.path.abspath(__file__))
db_file_path = os.path.join(app_directory, "client.db")
log_file_path = os.path.join(app_directory, "client.log")
interface = None
display_log = False
all_messages = {}
channel_list = []
notifications = set()
packet_buffer = []
node_list = []
myNodeNum = 0
selected_channel = 0
selected_message = 0
selected_node = 0
current_window = 0
interface = None
display_log = False
# User Configurable
db_file_path = os.path.join(app_directory, "client.db")
log_file_path = os.path.join(app_directory, "client.log")
message_prefix = ">>"
sent_message_prefix = message_prefix + " Sent"
notification_symbol = "*"
ack_implicit_str = "[◌]"
ack_str = "[✓]"
nak_str = "[x]"
ack_unknown_str = "[…]"
notification_symbol = "*"
node_list = []
ack_unknown_str = "[…]"

View File

@@ -10,7 +10,6 @@ def get_user_input(prompt):
# Create a new window for user input
input_win = curses.newwin(height, width, start_y, start_x)
input_win.clear()
input_win.border()
# Display the prompt
@@ -18,15 +17,12 @@ def get_user_input(prompt):
input_win.addstr(3, 2, "Enter value: ")
input_win.refresh()
# Enable user input
curses.echo()
curses.curs_set(1)
user_input = ""
while True:
key = input_win.getch(3, 15 + len(user_input)) # Adjust cursor position dynamically
if key == 27 or key == curses.KEY_LEFT: # ESC or Left Arrow
curses.noecho()
curses.curs_set(0)
return None # Exit without returning a value
elif key == ord('\n'): # Enter key
@@ -40,7 +36,6 @@ def get_user_input(prompt):
input_win.addstr(3, 15, user_input)
curses.curs_set(0)
curses.noecho()
# Clear the input window
input_win.clear()
@@ -102,7 +97,7 @@ def get_repeated_input(current_value):
repeated_win.clear()
repeated_win.border()
repeated_win.addstr(1, 2, "Enter comma-separated values:", curses.A_BOLD)
repeated_win.addstr(3, 2, f"Current: {', '.join(current_value)}")
repeated_win.addstr(3, 2, f"Current: {', '.join(map(str, current_value))}")
repeated_win.addstr(5, 2, f"New value: {user_input}")
repeated_win.refresh()

View File

@@ -3,7 +3,7 @@
'''
Contact - A Console UI for Meshtastic by http://github.com/pdxlocations
Powered by Meshtastic.org
V 1.0.1
V 1.0.3
'''
import curses

View File

@@ -5,13 +5,15 @@ from ui.curses_ui import draw_packetlog_win, draw_node_list, draw_messages_windo
from db_handler import save_message_to_db, maybe_store_nodeinfo_in_db
from datetime import datetime
def on_receive(packet, interface):
global nodes_win
# update packet log
# Update packet log
globals.packet_buffer.append(packet)
if len(globals.packet_buffer) > 20:
# trim buffer to 20 packets
# Trim buffer to 20 packets
globals.packet_buffer = globals.packet_buffer[-20:]
if globals.display_log:
@@ -20,10 +22,9 @@ def on_receive(packet, interface):
if 'decoded' not in packet:
return
# Assume any incoming packet could update the last seen time for a node, so we
# may need to reorder the list. This could probably be limited to specific packets.
# Assume any incoming packet could update the last seen time for a node
new_node_list = get_node_list()
if(new_node_list != globals.node_list):
if new_node_list != globals.node_list:
globals.node_list = new_node_list
draw_node_list()
@@ -42,6 +43,7 @@ def on_receive(packet, interface):
channel_number = packet['channel']
else:
channel_number = 0
if packet['to'] == globals.myNodeNum:
if packet['from'] in globals.channel_list:
pass
@@ -65,15 +67,35 @@ def on_receive(packet, interface):
if globals.channel_list[channel_number] not in globals.all_messages:
globals.all_messages[globals.channel_list[channel_number]] = []
# Timestamp handling
current_timestamp = int(packet['rxTime']) # Use the packet's rxTime for timestamp
current_hour = datetime.fromtimestamp(current_timestamp).strftime('%Y-%m-%d %H:00')
# Retrieve the last timestamp if available
channel_messages = globals.all_messages[globals.channel_list[channel_number]]
if channel_messages:
# Check the last entry for a timestamp
for entry in reversed(channel_messages):
if entry[0].startswith("--"):
last_hour = entry[0].strip("- ").strip()
break
else:
last_hour = None
else:
last_hour = None
# Add a new timestamp if it's a new hour
if last_hour != current_hour:
globals.all_messages[globals.channel_list[channel_number]].append((f"-- {current_hour} --", ""))
globals.all_messages[globals.channel_list[channel_number]].append((f"{globals.message_prefix} {message_from_string} ", message_string))
if(refresh_channels):
if refresh_channels:
draw_channel_list()
if(refresh_messages):
draw_messages_window()
if refresh_messages:
draw_messages_window(True)
save_message_to_db(globals.channel_list[channel_number], message_from_id, message_string)
except KeyError as e:
print(f"Error processing packet: {e}")
print(f"Error processing packet: {e}")

View File

@@ -1,3 +1,4 @@
from datetime import datetime
from meshtastic import BROADCAST_NUM
from db_handler import save_message_to_db, update_ack_nak
from meshtastic.protobuf import mesh_pb2, portnums_pb2
@@ -106,14 +107,14 @@ def on_response_traceroute(packet):
globals.all_messages[globals.channel_list[channel_number]] = []
globals.all_messages[globals.channel_list[channel_number]].append((f"{globals.message_prefix} {message_from_string}", msg_str))
if(refresh_channels):
if refresh_channels:
draw_channel_list()
if(refresh_messages):
draw_messages_window()
if refresh_messages:
draw_messages_window(True)
save_message_to_db(globals.channel_list[channel_number], packet['from'], msg_str)
def send_message(message, destination=BROADCAST_NUM, channel=0):
def send_message(message, destination=BROADCAST_NUM, channel=0):
myid = globals.myNodeNum
send_on_channel = 0
channel_id = globals.channel_list[channel]
@@ -136,11 +137,32 @@ def send_message(message, destination=BROADCAST_NUM, channel=0):
if channel_id not in globals.all_messages:
globals.all_messages[channel_id] = []
# Handle timestamp logic
current_timestamp = int(datetime.now().timestamp()) # Get current timestamp
current_hour = datetime.fromtimestamp(current_timestamp).strftime('%Y-%m-%d %H:00')
# Retrieve the last timestamp if available
channel_messages = globals.all_messages[channel_id]
if channel_messages:
# Check the last entry for a timestamp
for entry in reversed(channel_messages):
if entry[0].startswith("--"):
last_hour = entry[0].strip("- ").strip()
break
else:
last_hour = None
else:
last_hour = None
# Add a new timestamp if it's a new hour
if last_hour != current_hour:
globals.all_messages[channel_id].append((f"-- {current_hour} --", ""))
globals.all_messages[channel_id].append((globals.sent_message_prefix + globals.ack_unknown_str + ": ", message))
timestamp = save_message_to_db(channel_id, myid, message)
ack_naks[sent_message_data.id] = {'channel' : channel_id, 'messageIndex' : len(globals.all_messages[channel_id]) - 1, 'timestamp' : timestamp }
ack_naks[sent_message_data.id] = {'channel': channel_id, 'messageIndex': len(globals.all_messages[channel_id]) - 1, 'timestamp': timestamp}
def send_traceroute():
r = mesh_pb2.RouteDiscovery()

View File

@@ -40,14 +40,13 @@ def save_changes(interface, menu_path, modified_settings):
elif menu_path[1] == "User Settings": # for user configs
config_category = "User Settings"
long_name = modified_settings.get("longName", None)
short_name = modified_settings.get("shortName", None)
#TODO add is_licensed
node.setOwner(long_name, short_name, is_licensed=False)
logging.info(f"Updated {config_category} with Long Name: {long_name} and Short Name {short_name}")
long_name = modified_settings.get("longName")
short_name = modified_settings.get("shortName")
is_licensed = modified_settings.get("isLicensed")
node.setOwner(long_name, short_name, is_licensed)
logging.info(f"Updated {config_category} with Long Name: {long_name} and Short Name {short_name} and Licensed Mode {is_licensed}")
return
elif menu_path[1] == "Channels": # for channel configs
config_category = "Channels"
@@ -80,8 +79,6 @@ def save_changes(interface, menu_path, modified_settings):
else:
config_category = None
for config_item, new_value in modified_settings.items():
# Check if the category exists in localConfig
if hasattr(node.localConfig, config_category):

View File

@@ -1,14 +1,13 @@
import curses
import meshtastic.serial_interface
from save_to_radio import settings_factory_reset, settings_reboot, settings_reset_nodedb, settings_shutdown
from ui.menus import generate_menu_from_protobuf
from input_handlers import get_bool_selection, get_repeated_input, get_user_input, get_enum_input, get_fixed32_input
from save_to_radio import save_changes
from ui.colors import setup_colors
import logging
from save_to_radio import settings_factory_reset, settings_reboot, settings_reset_nodedb, settings_shutdown, save_changes
from ui.menus import generate_menu_from_protobuf
from input_handlers import get_bool_selection, get_repeated_input, get_user_input, get_enum_input, get_fixed32_input
from ui.colors import setup_colors
from utilities.arg_parser import setup_parser
from utilities.interfaces import initialize_interface
import globals
def display_menu(current_menu, menu_path, selected_index, show_save_option):
global menu_win
@@ -61,7 +60,7 @@ def display_menu(current_menu, menu_path, selected_index, show_save_option):
menu_win.refresh()
def settings_menu(sdscr, interface):
def settings_menu(stdscr, interface):
menu = generate_menu_from_protobuf(interface)
current_menu = menu["Main Menu"]
@@ -119,60 +118,50 @@ def settings_menu(sdscr, interface):
if confirmation == "True":
settings_reboot(interface)
logging.info(f"Node Reboot Requested by menu")
continue
break
elif selected_option == "Reset Node DB":
confirmation = get_bool_selection("Are you sure you want to Reset Node DB?", 0)
if confirmation == "True":
settings_reset_nodedb(interface)
logging.info(f"Node DB Reset Requested by menu")
continue
break
elif selected_option == "Shutdown":
confirmation = get_bool_selection("Are you sure you want to Shutdown?", 0)
if confirmation == "True":
settings_shutdown(interface)
logging.info(f"Node Shutdown Requested by menu")
continue
break
elif selected_option == "Factory Reset":
confirmation = get_bool_selection("Are you sure you want to Factory Reset?", 0)
if confirmation == "True":
settings_factory_reset(interface)
logging.info(f"Factory Reset Requested by menu")
continue
field_info = current_menu.get(selected_option)
break
field_info = current_menu.get(selected_option)
if isinstance(field_info, tuple):
field, current_value = field_info
if selected_option == 'longName' or selected_option == 'shortName':
new_value = get_user_input(f"Current value for {selected_option}: {current_value}")
modified_settings[selected_option] = (new_value)
current_menu[selected_option] = (field, new_value)
if selected_option in ['longName', 'shortName', 'isLicensed']:
if selected_option in ['longName', 'shortName']:
new_value = get_user_input(f"Current value for {selected_option}: {current_value}")
current_menu[selected_option] = (field, new_value)
elif selected_option == 'isLicensed':
new_value = get_bool_selection(f"Current value for {selected_option}: {current_value}", str(current_value))
new_value = new_value == "True"
current_menu[selected_option] = (field, new_value)
for option, (field, value) in current_menu.items():
modified_settings[option] = value
elif field.type == 8: # Handle boolean type
new_value = get_bool_selection(selected_option, str(current_value))
try:
# Validate and convert input to a valid boolean
if isinstance(new_value, str):
# Handle string representations of booleans
new_value_lower = new_value.lower()
if new_value_lower in ("true", "yes", "1", "on"):
new_value = True
elif new_value_lower in ("false", "no", "0", "off"):
new_value = False
else:
raise ValueError("Invalid string for boolean")
else:
# Convert other types directly to bool
new_value = bool(new_value)
except ValueError as e:
logging.info(f"Invalid input for boolean: {e}")
new_value = new_value == "True"
elif field.label == field.LABEL_REPEATED: # Handle repeated field
new_value = get_repeated_input(current_value)
new_value = current_value if new_value is None else [int(item) for item in new_value]
elif field.enum_type: # Enum field
enum_options = [v.name for v in field.enum_type.values]
@@ -189,20 +178,16 @@ def settings_menu(sdscr, interface):
new_value = get_user_input(f"Current value for {selected_option}: {current_value}")
new_value = current_value if new_value is None else float(new_value)
else: # Handle other field types
new_value = get_user_input(f"Current value for {selected_option}: {current_value}")
new_value = current_value if new_value is None else new_value
# Navigate to the correct nested dictionary based on the menu_path
current_nested = modified_settings
for key in menu_path[3:]: # Skip "Main Menu"
current_nested = current_nested.setdefault(key, {})
modified_settings = modified_settings.setdefault(key, {})
# Add the new value to the appropriate level
current_nested[selected_option] = new_value
modified_settings[selected_option] = new_value
# modified_settings[selected_option] = (new_value)
current_menu[selected_option] = (field, new_value)
else:
current_menu = current_menu[selected_option]
@@ -225,8 +210,11 @@ def settings_menu(sdscr, interface):
selected_index = 0
elif key == 27: # Escape key
menu_win.clear()
menu_win.refresh()
break
def main(stdscr):
logging.basicConfig( # Run `tail -f client.log` in another terminal to view live
filename="settings.log",
@@ -237,11 +225,11 @@ def main(stdscr):
curses.curs_set(0)
stdscr.keypad(True)
interface = meshtastic.serial_interface.SerialInterface()
parser = setup_parser()
args = parser.parse_args()
globals.interface = initialize_interface(args)
stdscr.clear()
stdscr.refresh()
settings_menu(stdscr, interface)
settings_menu(stdscr, globals.interface)
if __name__ == "__main__":
curses.wrapper(main)

View File

@@ -7,18 +7,65 @@ from message_handlers.tx_handler import send_message, send_traceroute
import ui.dialog
from ui.colors import setup_colors
def get_msg_window_lines():
packetlog_height = packetlog_win.getmaxyx()[0] 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(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 = channel_pad
select_len = 0
color = curses.color_pair(1)
if(window == 2):
pad = nodes_pad
select_len = len(get_name_from_number(globals.node_list[line], "long"))
if(window == 0):
channel = list(globals.all_messages.keys())[line]
win_width = channel_box.getmaxyx()[1]
if(isinstance(channel, int)):
channel = get_name_from_number(channel, type="long")
select_len = min(len(channel), win_width - 4)
if line == globals.selected_channel and highlight == False:
color = curses.color_pair(2)
pad.chgat(line, 1, select_len, color | curses.A_REVERSE if highlight else color)
def add_notification(channel_number):
handle_notification(channel_number, add=True)
globals.notifications.add(channel_number)
def remove_notification(channel_number):
handle_notification(channel_number, add=False)
channel_win.box()
def handle_notification(channel_number, add=True):
if add:
globals.notifications.add(channel_number) # Add the channel to the notification tracker
else:
globals.notifications.discard(channel_number) # Remove the channel from the notification tracker
globals.notifications.discard(channel_number)
def draw_text_field(win, text):
win.border()
@@ -59,142 +106,144 @@ def draw_splash(stdscr):
def draw_channel_list():
channel_win.clear()
win_height, win_width = channel_win.getmaxyx()
channel_pad.clear()
win_height, win_width = channel_box.getmaxyx()
start_index = max(0, globals.selected_channel - (win_height - 3)) # Leave room for borders
for i, channel in enumerate(list(globals.all_messages.keys())[start_index:], start=0):
channel_pad.resize(len(globals.all_messages), channel_box.getmaxyx()[1])
for i, channel in enumerate(list(globals.all_messages.keys())):
# Convert node number to long name if it's an integer
if isinstance(channel, int):
channel = get_name_from_number(channel, type='long')
# Determine whether to add the notification
notification = " " + globals.notification_symbol if start_index + i in globals.notifications else ""
notification = " " + globals.notification_symbol if i in globals.notifications else ""
# Truncate the channel name if it's too long to fit in the window
truncated_channel = channel[:win_width - 5] + '-' if len(channel) > win_width - 5 else channel
if i < win_height - 2 : # Check if there is enough space in the window
if start_index + i == globals.selected_channel and globals.current_window == 0:
channel_win.addstr(i + 1, 1, truncated_channel + notification, curses.color_pair(1) | curses.A_REVERSE)
if i == globals.selected_channel:
if globals.current_window == 0:
channel_pad.addstr(i, 1, truncated_channel + notification, curses.color_pair(1) | curses.A_REVERSE)
remove_notification(globals.selected_channel)
else:
channel_win.addstr(i + 1, 1, truncated_channel + notification, curses.color_pair(1))
channel_win.box()
channel_win.refresh()
channel_pad.addstr(i, 1, truncated_channel + notification, curses.color_pair(2))
else:
channel_pad.addstr(i, 1, truncated_channel + notification, curses.color_pair(1))
channel_box.attrset(curses.color_pair(2) if globals.current_window == 0 else curses.color_pair(0))
channel_box.box()
channel_box.attrset(curses.color_pair(0))
channel_box.refresh()
def draw_messages_window():
refresh_pad(0)
def draw_messages_window(scroll_to_bottom = False):
"""Update the messages window based on the selected channel and scroll position."""
messages_win.clear()
messages_pad.clear()
channel = globals.channel_list[globals.selected_channel]
if channel in globals.all_messages:
messages = globals.all_messages[channel]
num_messages = len(messages)
max_messages = messages_win.getmaxyx()[0] - 2 # Max messages that fit in the window
# Adjust for packetlog height if log is visible
if globals.display_log:
packetlog_height = packetlog_win.getmaxyx()[0]
max_messages -= packetlog_height - 1
if max_messages < 1:
max_messages = 1
msg_line_count = 0
# Calculate the scroll position based on the current selection
max_scroll_position = max(0, num_messages - max_messages)
start_index = max(0, min(globals.selected_message, max_scroll_position))
# Dynamically calculate max_messages based on visible messages and wraps
row = 1
visible_message_count = 0
for index, (prefix, message) in enumerate(messages[start_index:], start=start_index):
row = 0
for (prefix, message) in messages:
full_message = f"{prefix}{message}"
wrapped_lines = textwrap.wrap(full_message, messages_win.getmaxyx()[1] - 2)
if row + len(wrapped_lines) - 1 > messages_win.getmaxyx()[0] - 2: # Check if it fits in the window
break
visible_message_count += 1
row += len(wrapped_lines)
# Adjust max_messages to match visible messages
max_messages = visible_message_count
# Re-render the visible messages
row = 1
for index, (prefix, message) in enumerate(messages[start_index:start_index + max_messages], start=start_index):
full_message = f"{prefix}{message}"
wrapped_lines = textwrap.wrap(full_message, messages_win.getmaxyx()[1] - 2)
wrapped_lines = textwrap.wrap(full_message, messages_box.getmaxyx()[1] - 2)
msg_line_count += len(wrapped_lines)
messages_pad.resize(msg_line_count, messages_box.getmaxyx()[1])
for line in wrapped_lines:
# Highlight the row if it's the selected message
if index == globals.selected_message and globals.current_window == 1:
color = curses.A_REVERSE # Highlighted row color
else:
color = curses.color_pair(4) if prefix.startswith(globals.sent_message_prefix) else curses.color_pair(3)
messages_win.addstr(row, 1, line, color)
color = curses.color_pair(1) if prefix.startswith("--") else (curses.color_pair(3) if prefix.startswith(globals.sent_message_prefix) else curses.color_pair(2))
messages_pad.addstr(row, 1, line, color)
row += 1
messages_win.box()
messages_win.refresh()
messages_box.attrset(curses.color_pair(2) if globals.current_window == 1 else curses.color_pair(0))
messages_box.box()
messages_box.attrset(curses.color_pair(0))
messages_box.refresh()
if(scroll_to_bottom):
globals.selected_message = max(msg_line_count - get_msg_window_lines(), 0)
else:
globals.selected_message = max(min(globals.selected_message, msg_line_count - get_msg_window_lines()), 0)
refresh_pad(1)
draw_packetlog_win()
def draw_node_list():
nodes_win.clear()
win_height = nodes_win.getmaxyx()[0]
nodes_pad.clear()
win_height = nodes_box.getmaxyx()[0]
start_index = max(0, globals.selected_node - (win_height - 3)) # Calculate starting index based on selected node and window height
for i, node in enumerate(globals.node_list[start_index:], start=1):
if i < win_height - 1 : # Check if there is enough space in the window
if globals.selected_node + 1 == start_index + i and globals.current_window == 2:
nodes_win.addstr(i, 1, get_name_from_number(node, "long"), curses.color_pair(1) | curses.A_REVERSE)
else:
nodes_win.addstr(i, 1, get_name_from_number(node, "long"), curses.color_pair(1))
nodes_pad.resize(len(globals.node_list), nodes_box.getmaxyx()[1])
nodes_win.box()
nodes_win.refresh()
for i, node in enumerate(globals.node_list):
if globals.selected_node == i and globals.current_window == 2:
nodes_pad.addstr(i, 1, get_name_from_number(node, "long"), curses.color_pair(1) | curses.A_REVERSE)
else:
nodes_pad.addstr(i, 1, get_name_from_number(node, "long"), curses.color_pair(1))
nodes_box.attrset(curses.color_pair(2) if globals.current_window == 2 else curses.color_pair(0))
nodes_box.box()
nodes_box.attrset(curses.color_pair(0))
nodes_box.refresh()
def select_channels(direction):
channel_list_length = len(globals.channel_list)
globals.selected_channel += direction
refresh_pad(2)
if globals.selected_channel < 0:
globals.selected_channel = channel_list_length - 1
elif globals.selected_channel >= channel_list_length:
globals.selected_channel = 0
def select_channel(idx):
old_selected_channel = globals.selected_channel
globals.selected_channel = max(0, min(idx, len(globals.channel_list) - 1))
draw_messages_window(True)
draw_channel_list()
draw_messages_window()
# For now just re-draw channel list when clearing notifications, we can probably make this more efficient
if globals.selected_channel in globals.notifications:
remove_notification(globals.selected_channel)
draw_channel_list()
return
highlight_line(False, 0, old_selected_channel)
highlight_line(True, 0, globals.selected_channel)
refresh_pad(0)
def select_messages(direction):
messages_length = len(globals.all_messages[globals.channel_list[globals.selected_channel]])
def scroll_channels(direction):
new_selected_channel = globals.selected_channel + direction
if new_selected_channel < 0:
new_selected_channel = len(globals.channel_list) - 1
elif new_selected_channel >= len(globals.channel_list):
new_selected_channel = 0
select_channel(new_selected_channel)
def scroll_messages(direction):
globals.selected_message += direction
if globals.selected_message < 0:
globals.selected_message = messages_length - 1
elif globals.selected_message >= messages_length:
globals.selected_message = 0
msg_line_count = messages_pad.getmaxyx()[0]
globals.selected_message = max(0, min(globals.selected_message, msg_line_count - get_msg_window_lines()))
draw_messages_window()
refresh_pad(1)
def select_nodes(direction):
node_list_length = len(globals.node_list)
globals.selected_node += direction
def select_node(idx):
old_selected_node = globals.selected_node
globals.selected_node = max(0, min(idx, len(globals.node_list) - 1))
if globals.selected_node < 0:
globals.selected_node = node_list_length - 1
elif globals.selected_node >= node_list_length:
globals.selected_node = 0
highlight_line(False, 2, old_selected_node)
highlight_line(True, 2, globals.selected_node)
refresh_pad(2)
draw_node_list()
def scroll_nodes(direction):
new_selected_node = globals.selected_node + direction
if new_selected_node < 0:
new_selected_node = len(globals.node_list) - 1
elif new_selected_node >= len(globals.node_list):
new_selected_node = 0
select_node(new_selected_node)
def draw_packetlog_win():
@@ -241,7 +290,7 @@ def draw_packetlog_win():
def main_ui(stdscr):
global messages_win, nodes_win, channel_win, function_win, packetlog_win
global messages_pad, messages_box, nodes_pad, nodes_box, channel_pad, channel_box, function_win, packetlog_win
stdscr.keypad(True)
get_channels()
@@ -254,68 +303,106 @@ def main_ui(stdscr):
nodes_width = 5 * (width // 16)
messages_width = width - channel_width - nodes_width
channel_win = curses.newwin(height - 6, channel_width, 3, 0)
messages_win = curses.newwin(height - 6, messages_width, 3, channel_width)
packetlog_win = curses.newwin(int(height / 3), messages_width, height - int(height / 3) - 3, channel_width)
nodes_win = curses.newwin(height - 6, nodes_width, 3, channel_width + messages_width)
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)
# 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)
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)
draw_centered_text_field(function_win, f"↑→↓← = Select ENTER = Send ` = Settings / = Toggle Log ESC = Quit")
# Enable scrolling for messages and nodes windows
messages_win.scrollok(True)
nodes_win.scrollok(True)
channel_win.scrollok(True)
draw_channel_list()
draw_node_list()
draw_messages_window()
draw_centered_text_field(function_win, f"↑→↓← = Select ENTER = Send ` = Settings ^P = Packet Log ESC = Quit")
# Draw boxes around windows
channel_win.box()
channel_box.attrset(curses.color_pair(2))
channel_box.box()
channel_box.attrset(curses.color_pair(0))
entry_win.box()
messages_win.box()
nodes_win.box()
nodes_box.box()
messages_box.box()
function_win.box()
# Refresh all windows
entry_win.refresh()
messages_win.refresh()
nodes_win.refresh()
channel_win.refresh()
function_win.refresh()
channel_box.refresh()
function_win.refresh()
nodes_box.refresh()
messages_box.refresh()
input_text = ""
entry_win.keypad(True)
curses.curs_set(1)
draw_channel_list()
draw_node_list()
draw_messages_window(True)
while True:
draw_text_field(entry_win, f"Input: {input_text}")
draw_text_field(entry_win, f"Input: {input_text[-(width - 10):]}")
# Get user input from entry window
entry_win.move(1, len(input_text) + 8)
char = entry_win.getch()
char = entry_win.get_wch()
# draw_debug(f"Keypress: {char}")
if char == curses.KEY_UP:
if globals.current_window == 0:
select_channels(-1)
globals.selected_message = len(globals.all_messages[globals.channel_list[globals.selected_channel]]) - 1
scroll_channels(-1)
elif globals.current_window == 1:
select_messages(-1)
scroll_messages(-1)
elif globals.current_window == 2:
select_nodes(-1)
scroll_nodes(-1)
elif char == curses.KEY_DOWN:
if globals.current_window == 0:
select_channels(1)
globals.selected_message = len(globals.all_messages[globals.channel_list[globals.selected_channel]]) - 1
scroll_channels(1)
elif globals.current_window == 1:
select_messages(1)
scroll_messages(1)
elif globals.current_window == 2:
select_nodes(1)
scroll_nodes(1)
elif char == curses.KEY_HOME:
if globals.current_window == 0:
select_channel(0)
elif globals.current_window == 1:
globals.selected_message = 0
refresh_pad(1)
elif globals.current_window == 2:
select_node(0)
elif char == curses.KEY_END:
if globals.current_window == 0:
select_channel(len(globals.channel_list) - 1)
elif globals.current_window == 1:
msg_line_count = messages_pad.getmaxyx()[0]
globals.selected_message = max(msg_line_count - get_msg_window_lines(), 0)
refresh_pad(1)
elif globals.current_window == 2:
select_node(len(globals.node_list) - 1)
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
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
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
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
elif char == curses.KEY_LEFT or char == curses.KEY_RIGHT:
delta = -1 if char == curses.KEY_LEFT else 1
@@ -323,25 +410,57 @@ def main_ui(stdscr):
old_window = globals.current_window
globals.current_window = (globals.current_window + delta) % 3
if old_window == 0 or globals.current_window == 0:
draw_channel_list()
if old_window == 1 or globals.current_window == 1:
draw_messages_window()
if old_window == 2 or globals.current_window == 2:
draw_node_list()
if old_window == 0:
channel_box.attrset(curses.color_pair(0))
channel_box.box()
channel_box.refresh()
highlight_line(False, 0, globals.selected_channel)
refresh_pad(0)
if old_window == 1:
messages_box.attrset(curses.color_pair(0))
messages_box.box()
messages_box.refresh()
refresh_pad(1)
elif old_window == 2:
nodes_box.attrset(curses.color_pair(0))
nodes_box.box()
nodes_box.refresh()
highlight_line(False, 2, globals.selected_node)
refresh_pad(2)
if globals.current_window == 0:
channel_box.attrset(curses.color_pair(2))
channel_box.box()
channel_box.attrset(curses.color_pair(0))
channel_box.refresh()
highlight_line(True, 0, globals.selected_channel)
refresh_pad(0)
elif globals.current_window == 1:
messages_box.attrset(curses.color_pair(2))
messages_box.box()
messages_box.attrset(curses.color_pair(0))
messages_box.refresh()
refresh_pad(1)
elif globals.current_window == 2:
nodes_box.attrset(curses.color_pair(2))
nodes_box.box()
nodes_box.attrset(curses.color_pair(0))
nodes_box.refresh()
highlight_line(True, 2, globals.selected_node)
refresh_pad(2)
# Check for Esc
elif char == 27:
elif char == chr(27):
break
# Check for Ctrl + t
elif char == 20:
elif char == chr(20):
send_traceroute()
curses.curs_set(0) # Hide cursor
ui.dialog.dialog(stdscr, "Traceroute Sent", "Results will appear in messages window.\nNote: Traceroute is limited to once every 30 seconds.")
curses.curs_set(1) # Show cursor again
elif char == curses.KEY_ENTER or char == 10 or char == 13:
elif char in (chr(curses.KEY_ENTER), chr(10), chr(13)):
if globals.current_window == 2:
node_list = globals.node_list
if node_list[globals.selected_node] not in globals.channel_list:
@@ -354,19 +473,19 @@ def main_ui(stdscr):
draw_node_list()
draw_channel_list()
draw_messages_window()
draw_messages_window(True)
else:
# Enter key pressed, send user input as message
send_message(input_text, channel=globals.selected_channel)
draw_messages_window()
draw_messages_window(True)
# Clear entry window and reset input text
input_text = ""
entry_win.clear()
# entry_win.refresh()
elif char == curses.KEY_BACKSPACE or char == 127:
elif char in (curses.KEY_BACKSPACE, chr(127)):
if input_text:
input_text = input_text[:-1]
y, x = entry_win.getyx()
@@ -375,20 +494,25 @@ def main_ui(stdscr):
entry_win.move(y, x - 1)
entry_win.refresh()
elif char == 96: # ` Launch the settings interface
elif char == "`": # ` Launch the settings interface
curses.curs_set(0)
settings_menu(stdscr, globals.interface)
curses.curs_set(1)
elif char == 47:
elif char == chr(16):
# Display packet log
if globals.display_log is False:
globals.display_log = True
draw_messages_window()
draw_messages_window(True)
else:
globals.display_log = False
packetlog_win.clear()
draw_messages_window()
draw_messages_window(True)
else:
# Append typed character to input text
input_text += chr(char)
if(isinstance(char, str)):
input_text += char
else:
input_text += chr(char)

View File

@@ -61,7 +61,8 @@ def generate_menu_from_protobuf(interface):
menu_structure["Main Menu"]["User Settings"] = {
"longName": (None, current_user_config.get("longName", "Not Set")),
"shortName": (None, current_user_config.get("shortName", "Not Set"))
"shortName": (None, current_user_config.get("shortName", "Not Set")),
"isLicensed": (None, current_user_config.get("isLicensed", "False"))
}
else:

View File

@@ -57,17 +57,15 @@ def convert_to_camel_case(string):
def get_name_from_number(number, type='long'):
name = ""
for node in globals.interface.nodes.values():
nodes_snapshot = list(globals.interface.nodes.values())
for node in nodes_snapshot:
if number == node['num']:
if type == 'long':
name = node['user']['longName']
return name
return node['user']['longName']
elif type == 'short':
name = node['user']['shortName']
return name
return node['user']['shortName']
else:
pass
else:
name = str(decimal_to_hex(number)) # If long name not found, use the ID as string
return name
# If no match is found, use the ID as a string
return str(decimal_to_hex(number))