From 3ccc3256c56928324773473e5bd12f8dfb8b4be0 Mon Sep 17 00:00:00 2001 From: eunwoo song Date: Sun, 6 Sep 2026 23:54:22 +0900 Subject: [PATCH 1/2] feat: complete CLI config field names --- meshtastic/__main__.py | 24 ++++++++++++++++++-- meshtastic/tests/test_main.py | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index ca1680a58..3c6060f1d 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -63,7 +63,7 @@ have_powermon = False powermon_exception = e meter = None -from meshtastic.protobuf import admin_pb2, channel_pb2, clientonly_pb2, config_pb2, portnums_pb2, mesh_pb2 +from meshtastic.protobuf import admin_pb2, channel_pb2, clientonly_pb2, config_pb2, localonly_pb2, portnums_pb2, mesh_pb2 from meshtastic.version import get_active_version logger = logging.getLogger(__name__) @@ -1907,6 +1907,25 @@ def addImportExportArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentPar ) return parser +def _config_field_names() -> List[str]: + """Return shell-completion candidates derived from config descriptors.""" + names = set() + for config in (localonly_pb2.LocalConfig, localonly_pb2.LocalModuleConfig): + for section in config.DESCRIPTOR.fields: + if section.message_type is None: + continue + for field in section.message_type.fields: + snake_name = f"{section.name}.{field.name}" + names.add(snake_name) + names.add(meshtastic.util.snake_to_camel(snake_name)) + return sorted(names) + + +def _complete_config_fields(prefix: str, **_kwargs) -> List[str]: + """Complete static config paths without connecting to a device.""" + return [name for name in _config_field_names() if name.startswith(prefix)] + + def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: """Add arguments to do with configuring a device""" @@ -1915,7 +1934,7 @@ def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: "Arguments that concern general configuration of Meshtastic devices", ) - group.add_argument( + get_action = group.add_argument( "--get", help=( "Get a preferences field. Use an invalid field such as '0' to get a list of all fields." @@ -1925,6 +1944,7 @@ def addConfigArgs(parser: argparse.ArgumentParser) -> argparse.ArgumentParser: action="append", metavar="FIELD" ) + get_action.completer = _complete_config_fields # type: ignore[attr-defined] group.add_argument( "--set", diff --git a/meshtastic/tests/test_main.py b/meshtastic/tests/test_main.py index e0d65f60f..93075d1f5 100644 --- a/meshtastic/tests/test_main.py +++ b/meshtastic/tests/test_main.py @@ -26,6 +26,8 @@ tunnelMain, set_missing_flags_false, _profile_from_yaml, + _config_field_names, + _complete_config_fields, ) from meshtastic import mt_config @@ -46,6 +48,46 @@ # from ..remote_hardware import onGPIOreceive # from ..config_pb2 import Config + +@pytest.mark.unit +def test_config_field_names_follow_protobuf_descriptors(): + """Completion candidates include every current local configuration field.""" + expected = { + f"{section.name}.{field.name}" + for config in (LocalConfig, LocalModuleConfig) + for section in config.DESCRIPTOR.fields + if section.message_type is not None + for field in section.message_type.fields + } + + names = _config_field_names() + + assert expected <= set(names) + assert "power.ls_secs" in names + assert "power.lsSecs" in names + assert names == sorted(set(names)) + + +@pytest.mark.unit +def test_complete_config_fields_filters_by_prefix(): + """Shell completion returns only fields matching the typed prefix.""" + matches = list(_complete_config_fields("bluetooth.fixed")) + + assert matches + assert all(name.startswith("bluetooth.fixed") for name in matches) + assert "bluetooth.fixed_pin" in matches + assert "bluetooth.fixedPin" in matches + + +@pytest.mark.unit +def test_get_argument_uses_config_field_completer(): + """The --get argparse action exposes config candidates to argcomplete.""" + parser = mt_main.argparse.ArgumentParser() + mt_main.addConfigArgs(parser) + get_action = next(action for action in parser._actions if "--get" in action.option_strings) + + assert get_action.completer is _complete_config_fields + @pytest.mark.unit @pytest.mark.usefixtures("reset_mt_config") def test_main_init_parser_no_args(capsys): From 12ebe519d02193ee63bf8a56283e3090df98e84e Mon Sep 17 00:00:00 2001 From: eunwoo song Date: Mon, 7 Sep 2026 10:56:40 +0900 Subject: [PATCH 2/2] fix: type completion keyword arguments --- meshtastic/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meshtastic/__main__.py b/meshtastic/__main__.py index 3c6060f1d..46566878b 100644 --- a/meshtastic/__main__.py +++ b/meshtastic/__main__.py @@ -1921,7 +1921,7 @@ def _config_field_names() -> List[str]: return sorted(names) -def _complete_config_fields(prefix: str, **_kwargs) -> List[str]: +def _complete_config_fields(prefix: str, **_kwargs: object) -> List[str]: """Complete static config paths without connecting to a device.""" return [name for name in _config_field_names() if name.startswith(prefix)]