1
0
forked from iarv/contact

Compare commits

...

19 Commits
1.3.0 ... 1.3.2

Author SHA1 Message Date
pdxlocations
db01d241c7 bump version 2025-04-02 22:04:53 -07:00
pdxlocations
9044d8d380 Merge pull request #158 from pdxlocations:settings-flag
add settings flag
2025-04-02 22:03:56 -07:00
pdxlocations
0288a1d190 add settings flag 2025-04-02 22:03:28 -07:00
pdxlocations
3674afc216 remove version from main 2025-04-02 21:27:36 -07:00
pdxlocations
da24902bd0 Add Authors 2025-04-02 21:24:51 -07:00
pdxlocations
f9bc7f9be9 Merge pull request #157 from rfschmid:show-favorite-ignored-nodes
Color favorite/ignored nodes
2025-04-02 21:16:45 -07:00
pdxlocations
ffd28c02a3 Merge pull request #156 from rfschmid:rename-main-__main__
Rename main to __main__
2025-04-02 21:14:15 -07:00
Russell Schmidt
ecc360dba9 Color favorite/ignored nodes
Show favorite nodes in color node_favorite (green by default) and
ignored nodes in color node_favorite (red by default). Sort ignored
nodes at the bottom of the node list.
2025-04-02 12:16:15 -05:00
Russell Schmidt
696370308f Rename main to __main__
Most commonly, the __main__.py file is used to provide a command-line
interface for a package. __main__.py will be executed when the package
itself is invoked directly from the command line using the -m flag.
2025-04-02 12:05:01 -05:00
pdxlocations
5999deac1a bump version 2025-04-01 22:07:21 -07:00
pdxlocations
492c1d30d6 Merge pull request #149 from pdxlocations/pyproject-update
add home page
2025-04-01 22:05:38 -07:00
pdxlocations
9e3b684a5f add home page 2025-04-01 22:04:16 -07:00
pdxlocations
25f388ed23 Merge pull request #145 from rfschmid/fix-updating-data-for-nodes-not-working 2025-04-01 21:57:29 -07:00
pdxlocations
07fbdb92e3 Merge pull request #147 from rfschmid/add-ignore-node-support 2025-04-01 21:31:37 -07:00
Russell Schmidt
7c4cc1dd2f Merge 'upstream/main' into fix-updating-data-for-nodes-not-working 2025-04-01 22:52:39 -05:00
Russell Schmidt
06ce9f7ac2 Merge 'upstream/main' into add-ignore-node-support 2025-04-01 22:43:23 -05:00
Russell Schmidt
8ff55c3de9 Add ignore node support
Press Ctrl+G to ignore/unignore a node.
2025-03-31 21:56:23 -05:00
Russell Schmidt
d9088ccd68 Add favorite node support
Press Ctrl+F to favorite/unfavorite a node. Favorite nodes always appear
at the top of the node list
2025-03-31 21:35:15 -05:00
Russell Schmidt
db8496b2e3 Fix updating data on existing nodes
Since 4bc1654 changed the defaults of the parameters to
update_node_info_in_db(), any call to that funciton that didn't specify
a value for chat_archived would cause chat_archived to be set to 0,
because 0 is not None, we wouldn't preserve the existing value stored in
the DB. Update to use None paramters so we can tell what the caller
specified and did not specify again.
2025-03-31 19:23:15 -05:00
8 changed files with 122 additions and 21 deletions

6
.vscode/launch.json vendored
View File

@@ -6,8 +6,8 @@
"type": "debugpy",
"request": "launch",
"cwd": "${workspaceFolder}",
"module": "contact.main",
"args": []
"module": "contact.__main__",
"args": ["-c"]
}
]
}
}

View File

@@ -3,7 +3,6 @@
'''
Contact - A Console UI for Meshtastic by http://github.com/pdxlocations
Powered by Meshtastic.org
V 1.2.2
Meshtastic® is a registered trademark of Meshtastic LLC. Meshtastic software components are released under various licenses, see GitHub for details. No warranty is provided - use at your own risk.
'''
@@ -15,6 +14,7 @@ from pubsub import pub
import sys
import io
import logging
import subprocess
import traceback
import threading
@@ -42,7 +42,7 @@ if os.environ.get("COLORTERM") == "gnome-terminal":
# Run `tail -f client.log` in another terminal to view live
logging.basicConfig(
filename=config.log_file_path,
level=logging.INFO, # DEBUG, INFO, WARNING, ERROR, CRITICAL)
level=logging.DEBUG, # DEBUG, INFO, WARNING, ERROR, CRITICAL)
format="%(asctime)s - %(levelname)s - %(message)s"
)
@@ -58,6 +58,11 @@ def main(stdscr):
parser = setup_parser()
args = parser.parse_args()
# Check if --settings was passed and run settings.py as a subprocess
if getattr(args, 'settings', False):
subprocess.run([sys.executable, "-m", "contact.settings"], check=True)
return
logging.info("Initializing interface %s", args)
with globals.lock:
globals.interface = initialize_interface(args)

View File

@@ -7,6 +7,7 @@ from contact.settings import settings_menu
from contact.message_handlers.tx_handler import send_message, send_traceroute
from contact.ui.colors import setup_colors, get_color
from contact.utilities.db_handler import get_name_from_database, update_node_info_in_db, is_chat_archived
from contact.utilities.input_handlers import get_list_input
import contact.ui.default_config as config
import contact.ui.dialog
import contact.globals as globals
@@ -293,10 +294,57 @@ def main_ui(stdscr):
draw_channel_list()
draw_messages_window()
# ^/
elif char == chr(31):
if(globals.current_window == 2 or globals.current_window == 0):
search(globals.current_window)
# ^F
elif char == chr(6):
if globals.current_window == 2:
selectedNode = globals.interface.nodesByNum[globals.node_list[globals.selected_node]]
curses.curs_set(0)
if 'isFavorite' not in selectedNode or selectedNode['isFavorite'] == False:
confirmation = get_list_input(f"Set {get_name_from_database(globals.node_list[globals.selected_node])} as Favorite?", "no", ["yes", "no"])
if confirmation == "yes":
globals.interface.localNode.setFavorite(globals.node_list[globals.selected_node])
# Maybe we shouldn't be modifying the nodedb, but maybe it should update itself
globals.interface.nodesByNum[globals.node_list[globals.selected_node]]['isFavorite'] = True
refresh_node_list()
else:
confirmation = get_list_input(f"Remove {get_name_from_database(globals.node_list[globals.selected_node])} from Favorites?", "no", ["yes", "no"])
if confirmation == "yes":
globals.interface.localNode.removeFavorite(globals.node_list[globals.selected_node])
# Maybe we shouldn't be modifying the nodedb, but maybe it should update itself
globals.interface.nodesByNum[globals.node_list[globals.selected_node]]['isFavorite'] = False
refresh_node_list()
handle_resize(stdscr, False)
elif char == chr(7):
if globals.current_window == 2:
selectedNode = globals.interface.nodesByNum[globals.node_list[globals.selected_node]]
curses.curs_set(0)
if 'isIgnored' not in selectedNode or selectedNode['isIgnored'] == False:
confirmation = get_list_input(f"Set {get_name_from_database(globals.node_list[globals.selected_node])} as Ignored?", "no", ["yes", "no"])
if confirmation == "yes":
globals.interface.localNode.setIgnored(globals.node_list[globals.selected_node])
globals.interface.nodesByNum[globals.node_list[globals.selected_node]]['isIgnored'] = True
else:
confirmation = get_list_input(f"Remove {get_name_from_database(globals.node_list[globals.selected_node])} from Ignored?", "no", ["yes", "no"])
if confirmation == "yes":
globals.interface.localNode.removeIgnored(globals.node_list[globals.selected_node])
globals.interface.nodesByNum[globals.node_list[globals.selected_node]]['isIgnored'] = False
handle_resize(stdscr, False)
else:
# Append typed character to input text
if(isinstance(char, str)):
@@ -411,7 +459,12 @@ def draw_node_list():
node = globals.interface.nodesByNum[node_num]
secure = 'user' in node and 'publicKey' in node['user'] and node['user']['publicKey']
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))
color = "node_list"
if 'isFavorite' in node and node['isFavorite']:
color = "node_favorite"
if 'isIgnored' in node and node['isIgnored']:
color = "node_ignored"
nodes_pad.addstr(i, 1, node_str, get_color(color, reverse=globals.selected_node == i and globals.current_window == 2))
nodes_win.attrset(get_color("window_frame_selected") if globals.current_window == 2 else get_color("window_frame"))
nodes_win.box()
@@ -618,7 +671,7 @@ def draw_node_details():
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"]
cmds = ["↑→↓← = Select", " ENTER = Send", " ` = Settings", " ^P = Packet Log", " ESC = Quit", " ^t = Traceroute", " ^d = Archive Chat", " ^f = Favorite", " ^g = Ignore"]
function_str = ""
for s in cmds:
if(len(function_str) + len(s) < function_win.getmaxyx()[1] - 2):
@@ -672,9 +725,18 @@ def refresh_pad(window):
def highlight_line(highlight, window, line):
pad = nodes_pad
color = get_color("node_list")
select_len = nodes_win.getmaxyx()[1] - 2
if window == 2:
node_num = globals.node_list[line]
node = globals.interface.nodesByNum[node_num]
if 'isFavorite' in node and node['isFavorite']:
color = get_color("node_favorite")
if 'isIgnored' in node and node['isIgnored']:
color = get_color("node_ignored")
if(window == 0):
pad = channel_pad
color = get_color("channel_selected" if (line == globals.selected_channel and highlight == False) else "channel_list")
@@ -703,4 +765,4 @@ def draw_centered_text_field(win, text, y_offset, color):
def draw_debug(value):
function_win.addstr(1, 1, f"debug: {value} ")
function_win.refresh()
function_win.refresh()

View File

@@ -65,7 +65,9 @@ def initialize_config():
"settings_save": ["green", "black"],
"settings_breadcrumbs": ["white", "black"],
"settings_warning": ["red", "black"],
"settings_note": ["green", "black"]
"settings_note": ["green", "black"],
"node_favorite": ["green", "black"],
"node_ignored": ["red", "black"]
}
COLOR_CONFIG_LIGHT = {
"default": ["black", "white"],
@@ -89,7 +91,9 @@ def initialize_config():
"settings_save": ["green", "white"],
"settings_breadcrumbs": ["black", "white"],
"settings_warning": ["red", "white"],
"settings_note": ["green", "white"]
"settings_note": ["green", "white"],
"node_favorite": ["green", "white"],
"node_ignored": ["red", "white"]
}
COLOR_CONFIG_GREEN = {
"default": ["green", "black"],
@@ -115,7 +119,9 @@ def initialize_config():
"settings_save": ["green", "black"],
"settings_breadcrumbs": ["green", "black"],
"settings_warning": ["green", "black"],
"settings_note": ["green", "black"]
"settings_note": ["green", "black"],
"node_favorite": ["cyan", "white"],
"node_ignored": ["red", "white"]
}
default_config_variables = {
"db_file_path": db_file_path,

View File

@@ -33,5 +33,14 @@ def setup_parser():
default=None,
const="any"
)
parser.add_argument(
"--settings",
"--set",
"--control",
"-c",
help="Launch directly into the settings",
action="store_true"
)
return parser

View File

@@ -190,21 +190,15 @@ def maybe_store_nodeinfo_in_db(packet):
except Exception as e:
logging.error(f"Unexpected error in maybe_store_nodeinfo_in_db: {e}")
def update_node_info_in_db(user_id, long_name=None, short_name=None, hw_model="UNSET", is_licensed=0, role="CLIENT", public_key="", chat_archived=0):
def update_node_info_in_db(user_id, long_name=None, short_name=None, hw_model=None, is_licensed=None, role=None, public_key=None, chat_archived=None):
"""Update or insert node information into the database, preserving unchanged fields."""
try:
ensure_node_table_exists() # Ensure the table exists before any operation
if long_name == None:
long_name = "Meshtastic " + str(decimal_to_hex(user_id)[-4:])
if short_name == None:
short_name = str(decimal_to_hex(user_id)[-4:])
with sqlite3.connect(config.db_file_path) as db_connection:
db_cursor = db_connection.cursor()
table_name = f'"{globals.myNodeNum}_nodedb"' # Quote in case of numeric names
table_columns = [i[1] for i in db_cursor.execute(f'PRAGMA table_info({table_name})')]
if "chat_archived" not in table_columns:
update_table_query = f"ALTER TABLE {table_name} ADD COLUMN chat_archived INTEGER"
@@ -225,6 +219,14 @@ def update_node_info_in_db(user_id, long_name=None, short_name=None, hw_model="U
public_key = public_key if public_key is not None else existing_public_key
chat_archived = chat_archived if chat_archived is not None else existing_chat_archived
long_name = long_name if long_name is not None else "Meshtastic " + str(decimal_to_hex(user_id)[-4:])
short_name = short_name if short_name is not None else str(decimal_to_hex(user_id)[-4:])
hw_model = hw_model if hw_model is not None else "UNSET"
is_licensed = is_licensed if is_licensed is not None else 0
role = role if role is not None else "CLIENT"
public_key = public_key if public_key is not None else ""
chat_archived = chat_archived if chat_archived is not None else 0
# Upsert logic
upsert_query = f'''
INSERT INTO {table_name} (user_id, long_name, short_name, hw_model, is_licensed, role, public_key, chat_archived)

View File

@@ -47,7 +47,15 @@ def get_node_list():
return node['hopsAway'] if 'hopsAway' in node else 100
else:
return node
sorted_nodes = sorted(globals.interface.nodes.values(), key = node_sort)
# Move favorite nodes to the beginning
sorted_nodes = sorted(sorted_nodes, key = lambda node: node['isFavorite'] if 'isFavorite' in node else False, reverse = True)
# Move ignored nodes to the end
sorted_nodes = sorted(sorted_nodes, key = lambda node: node['isIgnored'] if 'isIgnored' in node else False)
node_list = [node['num'] for node in sorted_nodes if node['num'] != my_node_num]
return [my_node_num] + node_list # Ensuring your node is always first
return []

View File

@@ -1,9 +1,15 @@
[project]
name = "contact"
version = "1.3.0"
version = "1.3.2"
description = "This Python curses client for Meshtastic is a terminal-based client designed to manage device settings, enable mesh chat communication, and handle configuration backups and restores."
authors = [
{name = "Ben Lipsey",email = "ben@pdxlocations.com"}
{name = "Ben Lipsey",email = "ben@pdxlocations.com"},
{name = "Russell Schmidt"},
{name = "noon92"},
{name = "vidplace7"},
{name = "SpudGunMan"},
{name = "Ian McEwen"},
{name = "Nick Maloney"}
]
license = "GPL-3.0-only"
readme = "README.md"
@@ -12,10 +18,13 @@ dependencies = [
"meshtastic (>=2.6.0,<3.0.0)"
]
[project.urls]
Homepage = "https://github.com/pdxlocations/contact"
Issues = "https://github.com/pdxlocations/contact/issues"
[build-system]
requires = ["poetry-core>=2.0.0,<3.0.0"]
build-backend = "poetry.core.masonry.api"
[tool.poetry.scripts]
contact = "contact.main:start"
contact = "contact.__main__:start"