diff --git a/contact/__main__.py b/contact/__main__.py index b32b11d..6b3a9b1 100644 --- a/contact/__main__.py +++ b/contact/__main__.py @@ -31,7 +31,7 @@ from contact.settings import set_region from contact.ui.colors import setup_colors from contact.ui.contact_ui import main_ui from contact.ui.splash import draw_splash -from contact.utilities.arg_parser import setup_parser +from contact.utilities.arg_parser import __version__, setup_parser from contact.utilities.db_handler import init_nodedb, load_messages_from_db from contact.utilities.demo_data import build_demo_interface, configure_demo_database, seed_demo_messages from contact.utilities.input_handlers import get_list_input @@ -69,7 +69,7 @@ def prompt_region_if_unset(args: object, stdscr: Optional[curses.window] = None) set_region(interface_state.interface) close_interface(interface_state.interface) if stdscr is not None: - draw_splash(stdscr) + draw_splash(stdscr, version_str=__version__) interface_state.interface = reconnect_interface(args) @@ -128,7 +128,7 @@ def initialize_runtime_interface_with_retry(stdscr: curses.window, args: object) if choice == "Close": return None - draw_splash(stdscr) + draw_splash(stdscr, version_str=__version__) def initialize_globals(seed_demo: bool = False) -> None: @@ -160,7 +160,7 @@ def initialize_runtime_interface(args: object, stdscr=None): configure_demo_database() return build_demo_interface() if stdscr is not None: - return initialize_interface(args, status_callback=lambda status: draw_splash(stdscr, status)) + return initialize_interface(args, status_callback=lambda status: draw_splash(stdscr, status, version_str=__version__)) return initialize_interface(args) @@ -171,7 +171,7 @@ def main(stdscr: curses.window) -> None: try: setup_colors() ensure_min_rows(stdscr) - draw_splash(stdscr) + draw_splash(stdscr, version_str=__version__) args = setup_parser().parse_args() @@ -233,6 +233,10 @@ def start() -> None: setup_parser().print_help() sys.exit(0) + if "--version" in sys.argv or "-V" in sys.argv: + print(__version__) + sys.exit(0) + interrupted = False fatal_error = None diff --git a/contact/settings.py b/contact/settings.py index 3bd1412..82dd38e 100644 --- a/contact/settings.py +++ b/contact/settings.py @@ -10,7 +10,7 @@ from contact.ui.colors import setup_colors from contact.ui.control_ui import set_region, settings_menu from contact.ui.dialog import dialog from contact.ui.splash import draw_splash -from contact.utilities.arg_parser import setup_parser +from contact.utilities.arg_parser import __version__, setup_parser from contact.utilities.i18n import t from contact.utilities.input_handlers import get_list_input from contact.utilities.interfaces import initialize_interface, reconnect_interface @@ -31,13 +31,13 @@ def main(stdscr: curses.window) -> None: with contextlib.redirect_stdout(output_capture), contextlib.redirect_stderr(output_capture): setup_colors() ensure_min_rows(stdscr) - draw_splash(stdscr) + draw_splash(stdscr, version_str=__version__) curses.curs_set(0) stdscr.keypad(True) parser = setup_parser() args = parser.parse_args() - interface = initialize_interface(args, status_callback=lambda status: draw_splash(stdscr, status)) + interface = initialize_interface(args, status_callback=lambda status: draw_splash(stdscr, status, version_str=__version__)) if interface.localNode.localConfig.lora.region == 0: confirmation = get_list_input( @@ -48,7 +48,7 @@ def main(stdscr: curses.window) -> None: if confirmation == "Yes": set_region(interface) close_interface(interface) - draw_splash(stdscr) + draw_splash(stdscr, version_str=__version__) interface = reconnect_interface(args) stdscr.clear() stdscr.refresh() @@ -85,6 +85,10 @@ def ensure_min_rows(stdscr: curses.window, min_rows: int = 11) -> None: configure_logging(logging.WARNING) if __name__ == "__main__": + if "--version" in sys.argv or "-V" in sys.argv: + print(__version__) + sys.exit(0) + log_file = config.log_file_path log_f = open(log_file, "a", buffering=1) # Enable line-buffering for immediate log writes diff --git a/contact/ui/splash.py b/contact/ui/splash.py index 307e525..42327bd 100644 --- a/contact/ui/splash.py +++ b/contact/ui/splash.py @@ -3,7 +3,7 @@ import textwrap from contact.ui.colors import get_color -def draw_splash(stdscr: object, status: str = "connecting...") -> None: +def draw_splash(stdscr: object, status: str = "connecting...", version_str: str = "") -> None: """Draw the splash screen with a logo and connecting message.""" curses.curs_set(0) @@ -20,6 +20,12 @@ def draw_splash(stdscr: object, status: str = "connecting...") -> None: stdscr.addstr(start_y, start_x, message_1, get_color("splash_logo", bold=True)) stdscr.addstr(start_y + 1, start_x - 1, message_2, get_color("splash_logo", bold=True)) stdscr.addstr(start_y + 2, start_x - 2, message_3, get_color("splash_logo", bold=True)) + if version_str: + ver_line = f"v{version_str}" + ver_x = max(1, (width - len(ver_line)) // 2) + ver_y = start_y + 3 + if ver_y < height - 1: + stdscr.addstr(ver_y, ver_x, ver_line, get_color("splash_text")) status_lines = [] for line in status.splitlines(): status_lines.extend(textwrap.wrap(line, max(1, width - 4)) or [""]) diff --git a/contact/utilities/arg_parser.py b/contact/utilities/arg_parser.py index 9c563a0..fb6a030 100644 --- a/contact/utilities/arg_parser.py +++ b/contact/utilities/arg_parser.py @@ -1,4 +1,10 @@ from argparse import ArgumentParser +from importlib.metadata import version, PackageNotFoundError + +try: + __version__ = version("contact") +except PackageNotFoundError: + __version__ = "dev" def setup_parser() -> ArgumentParser: @@ -6,6 +12,7 @@ def setup_parser() -> ArgumentParser: add_help=True, epilog="If no connection arguments are specified, we attempt a serial connection and then a TCP connection to localhost.", ) + parser.add_argument("--version", "-V", action="version", version=__version__) connOuter = parser.add_argument_group( "Connection", "Optional arguments to specify a device to connect to and how." diff --git a/pyproject.toml b/pyproject.toml index f8a9cb5..5598525 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "contact" -version = "1.6.13" +version = "1.6.14" 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"} diff --git a/tests/test_arg_parser.py b/tests/test_arg_parser.py index f4aff98..2c8a02c 100644 --- a/tests/test_arg_parser.py +++ b/tests/test_arg_parser.py @@ -1,6 +1,6 @@ import unittest -from contact.utilities.arg_parser import setup_parser +from contact.utilities.arg_parser import __version__, setup_parser class ArgParserTests(unittest.TestCase): @@ -11,3 +11,16 @@ class ArgParserTests(unittest.TestCase): def test_demo_screenshot_defaults_to_false(self) -> None: args = setup_parser().parse_args([]) self.assertFalse(args.demo_screenshot) + + def test_version_flag_exits_zero(self) -> None: + with self.assertRaises(SystemExit) as ctx: + setup_parser().parse_args(["--version"]) + self.assertEqual(ctx.exception.code, 0) + + def test_version_short_flag_exits_zero(self) -> None: + with self.assertRaises(SystemExit) as ctx: + setup_parser().parse_args(["-V"]) + self.assertEqual(ctx.exception.code, 0) + + def test_version_is_not_none(self) -> None: + self.assertIsNotNone(__version__) diff --git a/tests/test_main.py b/tests/test_main.py index a76f5bb..f596916 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -59,7 +59,7 @@ class MainRuntimeTests(unittest.TestCase): self.assertIs(result, good_interface) get_list_input.assert_called_once() bad_interface.close.assert_called_once_with() - draw_splash.assert_called_once_with(stdscr) + draw_splash.assert_called_once_with(stdscr, version_str=entrypoint.__version__) def test_initialize_runtime_interface_with_retry_returns_none_when_user_closes(self) -> None: args = Namespace(demo_screenshot=False) @@ -91,7 +91,7 @@ class MainRuntimeTests(unittest.TestCase): set_region.assert_called_once_with(old_interface) old_interface.close.assert_called_once_with() - draw_splash.assert_called_once_with(stdscr) + draw_splash.assert_called_once_with(stdscr, version_str=entrypoint.__version__) reconnect.assert_called_once_with(args) self.assertIs(interface_state.interface, new_interface)