Compare commits

...

33 Commits

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
pdxlocations
f4115e48ad Merge pull request #148 from pdxlocations:poetry
Package for Poetry and Pypi
2025-04-01 15:53:20 -07:00
pdxlocations
857d8d0c04 update readme 2025-04-01 15:52:57 -07:00
pdxlocations
ec0554df14 working changes 2025-04-01 14:58:35 -07:00
pdxlocations
372204a684 mcontact -> contact 2025-04-01 14:57:51 -07: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
pdxlocations
2b0f6515af update workflow 2025-03-30 19:15:59 -07:00
pdxlocations
4cd7c4e24d change email 2025-03-30 16:28:26 -07:00
pdxlocations
92a30ad8b0 flix user config location 2025-03-27 21:14:51 -07:00
pdxlocations
2960553fef rm extra file 2025-03-27 21:08:26 -07:00
pdxlocations
37c5a7dbc3 fix some imports 2025-03-27 21:07:15 -07:00
pdxlocations
25240f211b add release.yaml 2025-03-27 20:55:50 -07:00
pdxlocations
c236a386a5 mcontact 2025-03-26 22:30:35 -07:00
pdxlocations
66a9954149 mtcontact to mcontact 2025-03-26 22:27:03 -07:00
pdxlocations
3df012dd69 add launch.json 2025-03-26 21:59:33 -07:00
vidplace7
6d204581ce Package with poetry 2025-03-25 09:00:37 -04:00
27 changed files with 372 additions and 84 deletions

143
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,143 @@
name: release
on:
push:
tags:
- "[0-9]+.[0-9]+.[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+a[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+b[0-9]+"
- "[0-9]+.[0-9]+.[0-9]+rc[0-9]+"
env:
PACKAGE_NAME: "contact"
OWNER: "pdxlocations"
jobs:
details:
runs-on: ubuntu-latest
outputs:
new_version: ${{ steps.release.outputs.new_version }}
suffix: ${{ steps.release.outputs.suffix }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- uses: actions/checkout@v2
- name: Extract tag and Details
id: release
run: |
if [ "${{ github.ref_type }}" = "tag" ]; then
TAG_NAME=${GITHUB_REF#refs/tags/}
NEW_VERSION=$(echo $TAG_NAME | awk -F'-' '{print $1}')
SUFFIX=$(echo $TAG_NAME | grep -oP '[a-z]+[0-9]+' || echo "")
echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT"
echo "suffix=$SUFFIX" >> "$GITHUB_OUTPUT"
echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
echo "Version is $NEW_VERSION"
echo "Suffix is $SUFFIX"
echo "Tag name is $TAG_NAME"
else
echo "No tag found"
exit 1
fi
check_pypi:
needs: details
runs-on: ubuntu-latest
steps:
- name: Fetch information from PyPI
run: |
response=$(curl -s https://pypi.org/pypi/${{ env.PACKAGE_NAME }}/json || echo "{}")
latest_previous_version=$(echo $response | jq --raw-output "select(.releases != null) | .releases | keys_unsorted | last")
if [ -z "$latest_previous_version" ]; then
echo "Package not found on PyPI."
latest_previous_version="0.0.0"
fi
echo "Latest version on PyPI: $latest_previous_version"
echo "latest_previous_version=$latest_previous_version" >> $GITHUB_ENV
- name: Compare versions and exit if not newer
run: |
NEW_VERSION=${{ needs.details.outputs.new_version }}
LATEST_VERSION=$latest_previous_version
if [ "$(printf '%s\n' "$LATEST_VERSION" "$NEW_VERSION" | sort -rV | head -n 1)" != "$NEW_VERSION" ] || [ "$NEW_VERSION" == "$LATEST_VERSION" ]; then
echo "The new version $NEW_VERSION is not greater than the latest version $LATEST_VERSION on PyPI."
exit 1
else
echo "The new version $NEW_VERSION is greater than the latest version $LATEST_VERSION on PyPI."
fi
setup_and_build:
needs: [details, check_pypi]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.12"
- name: Install Poetry
run: |
curl -sSL https://install.python-poetry.org | python3 -
echo "$HOME/.local/bin" >> $GITHUB_PATH
- name: Set project version with Poetry
run: |
poetry version ${{ needs.details.outputs.new_version }}
- name: Install dependencies
run: poetry install --sync --no-interaction
- name: Build source and wheel distribution
run: |
poetry build
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
pypi_publish:
name: Upload release to PyPI
needs: [setup_and_build, details]
runs-on: ubuntu-latest
environment:
name: release
permissions:
id-token: write
steps:
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Publish distribution to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
github_release:
name: Create GitHub Release
needs: [setup_and_build, details]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Create GitHub Release
id: create_release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create ${{ needs.details.outputs.tag_name }} dist/* --title ${{ needs.details.outputs.tag_name }} --generate-notes

3
.gitignore vendored
View File

@@ -7,4 +7,5 @@ client.db
client.log
settings.log
config.json
default_config.log
default_config.log
dist/

13
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
"version": "0.1.0",
"configurations": [
{
"name": "Python Debugger: Current File",
"type": "debugpy",
"request": "launch",
"cwd": "${workspaceFolder}",
"module": "contact.__main__",
"args": ["-c"]
}
]
}

View File

@@ -54,11 +54,11 @@ If no connection arguments are specified, the client will attempt a serial conne
### Example Usage
```sh
python main.py --port /dev/ttyUSB0
python main.py --host 192.168.1.1
python main.py --ble BlAddressOfDevice
contact --port /dev/ttyUSB0
contact --host 192.168.1.1
contact --ble BlAddressOfDevice
```
To quickly connect to localhost, use:
```sh
python main.py -t
contact -t
```

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,21 +14,22 @@ from pubsub import pub
import sys
import io
import logging
import subprocess
import traceback
import threading
from utilities.db_handler import init_nodedb, load_messages_from_db
from message_handlers.rx_handler import on_receive
from settings import set_region
from ui.curses_ui import main_ui
from ui.colors import setup_colors
from ui.splash import draw_splash
import ui.default_config as config
from utilities.arg_parser import setup_parser
from utilities.interfaces import initialize_interface
from utilities.input_handlers import get_list_input
from utilities.utils import get_channels, get_node_list, get_nodeNum
import globals
from contact.utilities.db_handler import init_nodedb, load_messages_from_db
from contact.message_handlers.rx_handler import on_receive
from contact.settings import set_region
from contact.ui.curses_ui import main_ui
from contact.ui.colors import setup_colors
from contact.ui.splash import draw_splash
import contact.ui.default_config as config
from contact.utilities.arg_parser import setup_parser
from contact.utilities.interfaces import initialize_interface
from contact.utilities.input_handlers import get_list_input
from contact.utilities.utils import get_channels, get_node_list, get_nodeNum
import contact.globals as globals
# Set ncurses compatibility settings
os.environ["NCURSES_NO_UTF8_ACS"] = "1"
@@ -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)
@@ -87,7 +92,7 @@ def main(stdscr):
logging.error("Console output before crash:\n%s", console_output)
raise # Re-raise only unexpected errors
if __name__ == "__main__":
def start():
log_file = config.log_file_path
log_f = open(log_file, "a", buffering=1) # Enable line-buffering for immediate log writes
@@ -103,4 +108,7 @@ if __name__ == "__main__":
except Exception as e:
logging.error("Fatal error in curses wrapper: %s", e)
logging.error("Traceback: %s", traceback.format_exc())
sys.exit(1) # Exit with an error code
sys.exit(1) # Exit with an error code
if __name__ == "__main__":
start()

View File

@@ -1,11 +1,11 @@
import logging
import time
from utilities.utils import refresh_node_list
from contact.utilities.utils import refresh_node_list
from datetime import datetime
from ui.curses_ui import draw_packetlog_win, draw_node_list, draw_messages_window, draw_channel_list, add_notification
from utilities.db_handler import save_message_to_db, maybe_store_nodeinfo_in_db, get_name_from_database, update_node_info_in_db
import ui.default_config as config
import globals
from contact.ui.curses_ui import draw_packetlog_win, draw_node_list, draw_messages_window, draw_channel_list, add_notification
from contact.utilities.db_handler import save_message_to_db, maybe_store_nodeinfo_in_db, get_name_from_database, update_node_info_in_db
import contact.ui.default_config as config
import contact.globals as globals
from datetime import datetime

View File

@@ -3,16 +3,16 @@ import google.protobuf.json_format
from meshtastic import BROADCAST_NUM
from meshtastic.protobuf import mesh_pb2, portnums_pb2
from utilities.db_handler import save_message_to_db, update_ack_nak, get_name_from_database, is_chat_archived, update_node_info_in_db
import ui.default_config as config
import globals
from contact.utilities.db_handler import save_message_to_db, update_ack_nak, get_name_from_database, is_chat_archived, update_node_info_in_db
import contact.ui.default_config as config
import contact.globals as globals
ack_naks = {}
# Note "onAckNak" has special meaning to the API, thus the nonstandard naming convention
# See https://github.com/meshtastic/python/blob/master/meshtastic/mesh_interface.py#L462
def onAckNak(packet):
from ui.curses_ui import draw_messages_window
from contact.ui.curses_ui import draw_messages_window
request = packet['decoded']['requestId']
if(request not in ack_naks):
return
@@ -43,7 +43,7 @@ def onAckNak(packet):
def on_response_traceroute(packet):
"""on response for trace route"""
from ui.curses_ui import draw_channel_list, draw_messages_window, add_notification
from contact.ui.curses_ui import draw_channel_list, draw_messages_window, add_notification
refresh_channels = False
refresh_messages = False

View File

@@ -5,13 +5,13 @@ import logging
import sys
import traceback
import ui.default_config as config
from utilities.input_handlers import get_list_input
from ui.colors import setup_colors
from ui.splash import draw_splash
from ui.control_ui import set_region, settings_menu
from utilities.arg_parser import setup_parser
from utilities.interfaces import initialize_interface
import contact.ui.default_config as config
from contact.utilities.input_handlers import get_list_input
from contact.ui.colors import setup_colors
from contact.ui.splash import draw_splash
from contact.ui.control_ui import set_region, settings_menu
from contact.utilities.arg_parser import setup_parser
from contact.utilities.interfaces import initialize_interface
def main(stdscr):

View File

@@ -1,5 +1,5 @@
import curses
import ui.default_config as config
import contact.ui.default_config as config
COLOR_MAP = {
"black": curses.COLOR_BLACK,

View File

@@ -5,14 +5,16 @@ import os
import re
import sys
from utilities.save_to_radio import save_changes
from utilities.config_io import config_export, config_import
from utilities.input_handlers import get_repeated_input, get_text_input, get_fixed32_input, get_list_input, get_admin_key_input
from ui.menus import generate_menu_from_protobuf
from ui.colors import get_color
from ui.dialog import dialog
from utilities.control_utils import parse_ini_file, transform_menu_path
from ui.user_config import json_editor
from contact.utilities.save_to_radio import save_changes
from contact.utilities.config_io import config_export, config_import
from contact.utilities.input_handlers import get_repeated_input, get_text_input, get_fixed32_input, get_list_input, get_admin_key_input
from contact.ui.menus import generate_menu_from_protobuf
from contact.ui.colors import get_color
from contact.ui.dialog import dialog
from contact.utilities.control_utils import parse_ini_file, transform_menu_path
from contact.ui.user_config import json_editor
import contact.localisations
# Constants
width = 80
@@ -27,8 +29,9 @@ parent_dir = os.path.abspath(os.path.join(script_dir, os.pardir))
# Paths
locals_dir = os.path.dirname(os.path.abspath(sys.argv[0])) # Current script directory
translation_file = os.path.join(locals_dir, "localisations", "en.ini")
config_folder = os.path.join(parent_dir, "node-configs")
translation_file = os.path.join(parent_dir, "localisations", "en.ini")
config_folder = os.path.join(locals_dir, "node-configs")
# Load translations
field_mapping, help_text = parse_ini_file(translation_file)

View File

@@ -2,14 +2,15 @@ import curses
import textwrap
import logging
import traceback
from utilities.utils import get_channels, get_readable_duration, get_time_ago, refresh_node_list
from settings import settings_menu
from message_handlers.tx_handler import send_message, send_traceroute
from ui.colors import setup_colors, get_color
from utilities.db_handler import get_name_from_database, update_node_info_in_db, is_chat_archived
import ui.default_config as config
import ui.dialog
import globals
from contact.utilities.utils import get_channels, get_readable_duration, get_time_ago, refresh_node_list
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
def handle_resize(stdscr, firstrun):
global messages_pad, messages_win, nodes_pad, nodes_win, channel_pad, channel_win, function_win, packetlog_win, entry_win
@@ -212,7 +213,7 @@ def main_ui(stdscr):
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.")
contact.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
handle_resize(stdscr, False)
@@ -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

@@ -1,5 +1,5 @@
import curses
from ui.colors import get_color
from contact.ui.colors import get_color
def dialog(stdscr, title, message):
height, width = stdscr.getmaxyx()

View File

@@ -1,5 +1,5 @@
import curses
from ui.colors import get_color
from contact.ui.colors import get_color
def draw_splash(stdscr):
curses.curs_set(0)

View File

@@ -1,9 +1,9 @@
import os
import json
import curses
from ui.colors import get_color, setup_colors, COLOR_MAP
from ui.default_config import format_json_single_line_arrays, loaded_config
from utilities.input_handlers import get_list_input
from contact.ui.colors import get_color, setup_colors, COLOR_MAP
from contact.ui.default_config import format_json_single_line_arrays, loaded_config
from contact.utilities.input_handlers import get_list_input
width = 60
save_option_text = "Save Changes"
@@ -196,7 +196,10 @@ def json_editor(stdscr):
menu_path = ["App Settings"]
selected_index = 0 # Track the selected option
file_path = "config.json"
script_dir = os.path.dirname(os.path.abspath(__file__))
parent_dir = os.path.abspath(os.path.join(script_dir, os.pardir))
file_path = os.path.join(parent_dir, "config.json")
# file_path = "config.json"
show_save_option = True # Always show the Save button
# Ensure the file exists

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

@@ -3,9 +3,9 @@ import time
import logging
from datetime import datetime
from utilities.utils import decimal_to_hex
import ui.default_config as config
import globals
from contact.utilities.utils import decimal_to_hex
import contact.ui.default_config as config
import contact.globals as globals
def get_table_name(channel):
# Construct the table name
@@ -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

@@ -3,7 +3,7 @@ import binascii
import curses
import ipaddress
import re
from ui.colors import get_color
from contact.ui.colors import get_color
def wrap_text(text, wrap_width):
"""Wraps text while preserving spaces and breaking long words."""

View File

@@ -1,6 +1,6 @@
import logging
import meshtastic.serial_interface, meshtastic.tcp_interface, meshtastic.ble_interface
import globals
import contact.globals as globals
def initialize_interface(args):
try:

View File

@@ -1,7 +1,7 @@
import globals
import contact.globals as globals
import datetime
from meshtastic.protobuf import config_pb2
import ui.default_config as config
import contact.ui.default_config as config
def get_channels():
"""Retrieve channels from the node and update globals.channel_list and globals.all_messages."""
@@ -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 []

30
pyproject.toml Normal file
View File

@@ -0,0 +1,30 @@
[project]
name = "contact"
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 = "Russell Schmidt"},
{name = "noon92"},
{name = "vidplace7"},
{name = "SpudGunMan"},
{name = "Ian McEwen"},
{name = "Nick Maloney"}
]
license = "GPL-3.0-only"
readme = "README.md"
requires-python = ">=3.9,<3.14"
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"