zmk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. zmk/__init__.py +0 -0
  2. zmk/_version.py +16 -0
  3. zmk/backports.py +10 -0
  4. zmk/build.py +174 -0
  5. zmk/commands/__init__.py +28 -0
  6. zmk/commands/cd.py +56 -0
  7. zmk/commands/code.py +197 -0
  8. zmk/commands/config.py +83 -0
  9. zmk/commands/download.py +26 -0
  10. zmk/commands/init.py +134 -0
  11. zmk/commands/keyboard/__init__.py +21 -0
  12. zmk/commands/keyboard/add.py +164 -0
  13. zmk/commands/keyboard/list.py +201 -0
  14. zmk/commands/keyboard/new.py +356 -0
  15. zmk/commands/keyboard/remove.py +28 -0
  16. zmk/commands/module/__init__.py +19 -0
  17. zmk/commands/module/add.py +162 -0
  18. zmk/commands/module/list.py +36 -0
  19. zmk/commands/module/remove.py +113 -0
  20. zmk/commands/west.py +45 -0
  21. zmk/config.py +123 -0
  22. zmk/exceptions.py +52 -0
  23. zmk/hardware.py +234 -0
  24. zmk/main.py +57 -0
  25. zmk/menu.py +482 -0
  26. zmk/prompt.py +17 -0
  27. zmk/repo.py +238 -0
  28. zmk/styles.py +21 -0
  29. zmk/templates/__init__.py +82 -0
  30. zmk/templates/board/nrf52840/Kconfig +22 -0
  31. zmk/templates/board/nrf52840/Kconfig.defconfig +22 -0
  32. zmk/templates/board/nrf52840/board.cmake +5 -0
  33. zmk/templates/board/nrf52840/board.yaml +18 -0
  34. zmk/templates/board/nrf52840/board_defconfig +34 -0
  35. zmk/templates/board/nrf52840/common_inner.dtsi +29 -0
  36. zmk/templates/board/nrf52840/common_outer.dtsi +59 -0
  37. zmk/templates/board/nrf52840/pinctrl.dtsi +26 -0
  38. zmk/templates/board/nrf52840/split/${id}.conf +1 -0
  39. zmk/templates/board/nrf52840/split/${id}.dtsi +26 -0
  40. zmk/templates/board/nrf52840/split/${id}.keymap +1 -0
  41. zmk/templates/board/nrf52840/split/${id}.yaml +1 -0
  42. zmk/templates/board/nrf52840/split/${id}.zmk.yml +8 -0
  43. zmk/templates/board/nrf52840/split/${id}_left-pinctrl.dtsi +1 -0
  44. zmk/templates/board/nrf52840/split/${id}_left.dts +1 -0
  45. zmk/templates/board/nrf52840/split/${id}_left_defconfig +1 -0
  46. zmk/templates/board/nrf52840/split/${id}_right-pinctrl.dtsi +1 -0
  47. zmk/templates/board/nrf52840/split/${id}_right.dts +1 -0
  48. zmk/templates/board/nrf52840/split/${id}_right_defconfig +1 -0
  49. zmk/templates/board/nrf52840/split/Kconfig +3 -0
  50. zmk/templates/board/nrf52840/split/Kconfig.board +7 -0
  51. zmk/templates/board/nrf52840/split/Kconfig.defconfig +16 -0
  52. zmk/templates/board/nrf52840/split/board.cmake +1 -0
  53. zmk/templates/board/nrf52840/unibody/${id}-pinctrl.dtsi +1 -0
  54. zmk/templates/board/nrf52840/unibody/${id}.conf +1 -0
  55. zmk/templates/board/nrf52840/unibody/${id}.dts +24 -0
  56. zmk/templates/board/nrf52840/unibody/${id}.keymap +1 -0
  57. zmk/templates/board/nrf52840/unibody/${id}.yaml +1 -0
  58. zmk/templates/board/nrf52840/unibody/${id}.zmk.yml +4 -0
  59. zmk/templates/board/nrf52840/unibody/${id}_defconfig +1 -0
  60. zmk/templates/board/nrf52840/unibody/Kconfig +3 -0
  61. zmk/templates/board/nrf52840/unibody/Kconfig.board +3 -0
  62. zmk/templates/board/nrf52840/unibody/Kconfig.defconfig +9 -0
  63. zmk/templates/board/nrf52840/unibody/board.cmake +1 -0
  64. zmk/templates/board/other/Kconfig +10 -0
  65. zmk/templates/board/other/Kconfig.defconfig +22 -0
  66. zmk/templates/board/other/board.cmake +6 -0
  67. zmk/templates/board/other/board.yaml +19 -0
  68. zmk/templates/board/other/board_defconfig +15 -0
  69. zmk/templates/board/other/pinctrl.dtsi +8 -0
  70. zmk/templates/board/other/split/${id}.conf +1 -0
  71. zmk/templates/board/other/split/${id}.dtsi +5 -0
  72. zmk/templates/board/other/split/${id}.keymap +1 -0
  73. zmk/templates/board/other/split/${id}.yaml +1 -0
  74. zmk/templates/board/other/split/${id}.zmk.yml +8 -0
  75. zmk/templates/board/other/split/${id}_left-pinctrl.dtsi +1 -0
  76. zmk/templates/board/other/split/${id}_left.dts +1 -0
  77. zmk/templates/board/other/split/${id}_left_defconfig +1 -0
  78. zmk/templates/board/other/split/${id}_right-pinctrl.dtsi +1 -0
  79. zmk/templates/board/other/split/${id}_right.dts +1 -0
  80. zmk/templates/board/other/split/${id}_right_defconfig +1 -0
  81. zmk/templates/board/other/split/Kconfig +3 -0
  82. zmk/templates/board/other/split/Kconfig.board +13 -0
  83. zmk/templates/board/other/split/Kconfig.defconfig +16 -0
  84. zmk/templates/board/other/split/board.cmake +1 -0
  85. zmk/templates/board/other/unibody/${id}-pinctrl.dtsi +1 -0
  86. zmk/templates/board/other/unibody/${id}.conf +1 -0
  87. zmk/templates/board/other/unibody/${id}.dts +5 -0
  88. zmk/templates/board/other/unibody/${id}.keymap +1 -0
  89. zmk/templates/board/other/unibody/${id}.yaml +1 -0
  90. zmk/templates/board/other/unibody/${id}.zmk.yml +4 -0
  91. zmk/templates/board/other/unibody/${id}_defconfig +1 -0
  92. zmk/templates/board/other/unibody/Kconfig +3 -0
  93. zmk/templates/board/other/unibody/Kconfig.board +8 -0
  94. zmk/templates/board/other/unibody/Kconfig.defconfig +9 -0
  95. zmk/templates/board/other/unibody/board.cmake +1 -0
  96. zmk/templates/common/board.dts +32 -0
  97. zmk/templates/common/board_left.dts +7 -0
  98. zmk/templates/common/board_right.dts +7 -0
  99. zmk/templates/common/board_split.dtsi +7 -0
  100. zmk/templates/common/default.conf +12 -0
  101. zmk/templates/common/hardware.zmk.yml +19 -0
  102. zmk/templates/common/keymap.dtsi +23 -0
  103. zmk/templates/common/keymap_split.dtsi +9 -0
  104. zmk/templates/common/kscan.dtsi +24 -0
  105. zmk/templates/common/kscan_split_common.dtsi +20 -0
  106. zmk/templates/common/kscan_split_left.dtsi +9 -0
  107. zmk/templates/common/kscan_split_right.dtsi +14 -0
  108. zmk/templates/common/matrix_transform.dtsi +12 -0
  109. zmk/templates/common/matrix_transform_split.dtsi +12 -0
  110. zmk/templates/common/shield.overlay +24 -0
  111. zmk/templates/common/shield_left.overlay +6 -0
  112. zmk/templates/common/shield_right.overlay +6 -0
  113. zmk/templates/shield/split/${id}.conf +1 -0
  114. zmk/templates/shield/split/${id}.dtsi +7 -0
  115. zmk/templates/shield/split/${id}.keymap +1 -0
  116. zmk/templates/shield/split/${id}.zmk.yml +5 -0
  117. zmk/templates/shield/split/${id}_left.overlay +1 -0
  118. zmk/templates/shield/split/${id}_right.overlay +1 -0
  119. zmk/templates/shield/split/Kconfig.defconfig +16 -0
  120. zmk/templates/shield/split/Kconfig.shield +5 -0
  121. zmk/templates/shield/unibody/${id}.conf +1 -0
  122. zmk/templates/shield/unibody/${id}.keymap +1 -0
  123. zmk/templates/shield/unibody/${id}.overlay +4 -0
  124. zmk/templates/shield/unibody/${id}.zmk.yml +5 -0
  125. zmk/templates/shield/unibody/Kconfig.defconfig +6 -0
  126. zmk/templates/shield/unibody/Kconfig.shield +2 -0
  127. zmk/terminal.py +179 -0
  128. zmk/util.py +53 -0
  129. zmk/yaml.py +64 -0
  130. zmk-0.1.0.dist-info/LICENSE +21 -0
  131. zmk-0.1.0.dist-info/METADATA +290 -0
  132. zmk-0.1.0.dist-info/RECORD +135 -0
  133. zmk-0.1.0.dist-info/WHEEL +5 -0
  134. zmk-0.1.0.dist-info/entry_points.txt +2 -0
  135. zmk-0.1.0.dist-info/top_level.txt +1 -0
zmk/__init__.py ADDED
File without changes
zmk/_version.py ADDED
@@ -0,0 +1,16 @@
1
+ # file generated by setuptools_scm
2
+ # don't change, don't track in version control
3
+ TYPE_CHECKING = False
4
+ if TYPE_CHECKING:
5
+ from typing import Tuple, Union
6
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
7
+ else:
8
+ VERSION_TUPLE = object
9
+
10
+ version: str
11
+ __version__: str
12
+ __version_tuple__: VERSION_TUPLE
13
+ version_tuple: VERSION_TUPLE
14
+
15
+ __version__ = version = '0.1.0'
16
+ __version_tuple__ = version_tuple = (0, 1, 0)
zmk/backports.py ADDED
@@ -0,0 +1,10 @@
1
+ """
2
+ Backports from Python > 3.10.
3
+ """
4
+
5
+ try:
6
+ # pylint: disable=unused-import
7
+ from enum import StrEnum
8
+ except ImportError:
9
+ # pylint: disable=unused-import
10
+ from backports.strenum import StrEnum
zmk/build.py ADDED
@@ -0,0 +1,174 @@
1
+ """
2
+ Build matrix processing.
3
+ """
4
+
5
+ import collections.abc
6
+ from dataclasses import asdict, dataclass, field
7
+ from pathlib import Path
8
+ from typing import Any, Iterable, Optional
9
+
10
+ import dacite
11
+
12
+ from .repo import Repo
13
+ from .yaml import YAML
14
+
15
+
16
+ @dataclass
17
+ class BuildItem:
18
+ """An item in the build matrix"""
19
+
20
+ board: str
21
+ shield: Optional[str] = None
22
+ snippet: Optional[str] = None
23
+ cmake_args: Optional[str] = None
24
+ artifact_name: Optional[str] = None
25
+
26
+ def __rich__(self):
27
+ parts = []
28
+ parts.append(self.board)
29
+
30
+ if self.shield:
31
+ parts.append(self.shield)
32
+
33
+ if self.snippet:
34
+ parts.append(f"[dim]snippet: {self.snippet}[/dim]")
35
+
36
+ if self.artifact_name:
37
+ parts.append(f"[dim]artifact-name: {self.artifact_name}[/dim]")
38
+
39
+ if self.cmake_args:
40
+ parts.append(f"[dim]cmake-args: {self.cmake_args}[/dim]")
41
+
42
+ return "[dim], [/dim]".join(parts)
43
+
44
+
45
+ @dataclass
46
+ class _BuildMatrixWrapper:
47
+ include: list[BuildItem] = field(default_factory=list)
48
+
49
+
50
+ class BuildMatrix:
51
+ """Interface to read and edit ZMK's build.yaml file"""
52
+
53
+ _path: Path
54
+ _yaml: YAML
55
+ _data: Any
56
+
57
+ @classmethod
58
+ def from_repo(cls, repo: Repo):
59
+ """Get the build matrix for a repo"""
60
+ return cls(repo.build_matrix_path)
61
+
62
+ def __init__(self, path: Path) -> None:
63
+ self._path = path
64
+ self._yaml = YAML(typ="rt")
65
+ self._yaml.indent(mapping=2, sequence=4, offset=2)
66
+ try:
67
+ self._data = self._yaml.load(self._path)
68
+ except FileNotFoundError:
69
+ self._data = None
70
+
71
+ def write(self):
72
+ """Updated the YAML file, creating it if necessary"""
73
+ self._yaml.dump(self._data, self._path)
74
+
75
+ @property
76
+ def path(self):
77
+ """Path to the matrix's YAML file"""
78
+ return self._path
79
+
80
+ @property
81
+ def include(self) -> list[BuildItem]:
82
+ """List of build items in the matrix"""
83
+ normalized = _keys_to_python(self._data)
84
+ if not normalized:
85
+ return []
86
+
87
+ wrapper = dacite.from_dict(_BuildMatrixWrapper, normalized)
88
+ return wrapper.include
89
+
90
+ def has_item(self, item: BuildItem):
91
+ """Get whether the matrix has a build item"""
92
+ return item in self.include
93
+
94
+ def append(self, items: BuildItem | Iterable[BuildItem]) -> list[BuildItem]:
95
+ """
96
+ Add build items to the matrix.
97
+
98
+ :return: the items that were added.
99
+ """
100
+ items = [items] if isinstance(items, BuildItem) else items
101
+
102
+ include = self.include
103
+ items = [i for i in items if i not in include]
104
+
105
+ if not items:
106
+ return []
107
+
108
+ if not self._data:
109
+ self._data = self._yaml.map()
110
+
111
+ if "include" not in self._data:
112
+ self._data["include"] = self._yaml.seq()
113
+
114
+ self._data["include"].extend([_to_yaml(i) for i in items])
115
+ return items
116
+
117
+ def remove(self, items: BuildItem | Iterable[BuildItem]) -> list[BuildItem]:
118
+ """
119
+ Remove build items from the matrix.
120
+
121
+ :return: the items that were removed.
122
+ """
123
+ if not self._data or "include" not in self._data:
124
+ return False
125
+
126
+ removed = []
127
+ items = [items] if isinstance(items, BuildItem) else items
128
+
129
+ # TODO: there's probably a more efficient way to do this, but this is easy
130
+ for item in items:
131
+ try:
132
+ index = self.include.index(item)
133
+ del self._data["include"][index]
134
+ removed.append(item)
135
+ except ValueError:
136
+ pass
137
+
138
+ return removed
139
+
140
+
141
+ def _keys_to_python(data: Any):
142
+ """
143
+ Fix any keys with hyphens to underscores so that dacite.from_dict() will
144
+ work correctly.
145
+ """
146
+
147
+ def fix_key(key: str):
148
+ return key.replace("-", "_")
149
+
150
+ match data:
151
+ case str():
152
+ return data
153
+
154
+ case collections.abc.Sequence():
155
+ return [_keys_to_python(i) for i in data]
156
+
157
+ case collections.abc.Mapping():
158
+ return {fix_key(k): _keys_to_python(v) for k, v in data.items()}
159
+
160
+ case _:
161
+ return data
162
+
163
+
164
+ def _to_yaml(item: BuildItem):
165
+ """
166
+ Convert a BuildItem to a dict with keys changed back from underscores to hyphens.
167
+ """
168
+
169
+ def fix_key(key: str):
170
+ return key.replace("_", "-")
171
+
172
+ data = asdict(item)
173
+
174
+ return {fix_key(k): v for k, v in data.items() if v is not None}
@@ -0,0 +1,28 @@
1
+ """
2
+ ZMK CLI subcommands.
3
+ """
4
+
5
+ import typer
6
+
7
+ from . import cd, code, config, download, init, keyboard, module, west
8
+
9
+
10
+ def register(app: typer.Typer):
11
+ """Register all commands with the app"""
12
+ app.command()(cd.cd)
13
+ app.command()(code.code)
14
+ app.command()(config.config)
15
+ app.command()(download.download)
16
+ app.command(name="dl")(download.download)
17
+ app.command()(init.init)
18
+ app.command()(west.update)
19
+ app.command(
20
+ add_help_option=False,
21
+ context_settings={
22
+ "allow_extra_args": True,
23
+ "ignore_unknown_options": True,
24
+ },
25
+ )(west.west)
26
+
27
+ app.add_typer(keyboard.app)
28
+ app.add_typer(module.app)
zmk/commands/cd.py ADDED
@@ -0,0 +1,56 @@
1
+ """
2
+ "zmk cd" command.
3
+ """
4
+
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import shellingham
11
+ import typer
12
+
13
+ from ..config import Config
14
+ from ..exceptions import FatalError, FatalHomeMissing, FatalHomeNotSet
15
+
16
+
17
+ def cd(ctx: typer.Context):
18
+ """Go to the ZMK config repo."""
19
+ if not sys.stdout.isatty():
20
+ raise FatalError(
21
+ "This command can only be used from an interactive shell. "
22
+ 'Use "cd $(zmk config user.home)" instead.'
23
+ )
24
+
25
+ cfg = ctx.find_object(Config)
26
+ home = cfg.home_path
27
+
28
+ if home is None:
29
+ raise FatalHomeNotSet()
30
+
31
+ if not home.is_dir():
32
+ raise FatalHomeMissing(home)
33
+
34
+ if home == Path(os.getcwd()):
35
+ # Already in the home directory. Nothing to do.
36
+ return
37
+
38
+ os.chdir(home)
39
+
40
+ try:
41
+ _, shell = shellingham.detect_shell()
42
+ except shellingham.ShellDetectionFailure:
43
+ shell = _default_shell()
44
+
45
+ if os.name == "nt":
46
+ subprocess.run([shell], env=os.environ.copy(), check=False)
47
+ else:
48
+ os.execl(shell, shell)
49
+
50
+
51
+ def _default_shell():
52
+ if os.name == "posix":
53
+ return os.environ["SHELL"]
54
+ if os.name == "nt":
55
+ return os.environ["COMSPEC"]
56
+ raise NotImplementedError(f"Don't know how to get a shell for OS {os.name!r}")
zmk/commands/code.py ADDED
@@ -0,0 +1,197 @@
1
+ """
2
+ "zmk code" command.
3
+ """
4
+
5
+ import platform
6
+ import shlex
7
+ import shutil
8
+ import subprocess
9
+ from configparser import NoOptionError
10
+ from dataclasses import dataclass, field
11
+ from enum import Flag, auto
12
+ from typing import Annotated, Callable, Optional
13
+
14
+ import rich
15
+ import typer
16
+ from rich.markdown import Markdown
17
+
18
+ from ..config import Config, Settings
19
+ from ..exceptions import FatalError
20
+ from ..menu import show_menu
21
+ from ..repo import Repo
22
+
23
+
24
+ def code(
25
+ ctx: typer.Context,
26
+ keyboard: Annotated[
27
+ Optional[str],
28
+ typer.Argument(
29
+ help="Name of the keyboard to edit. If omitted, opens the repo directory.",
30
+ ),
31
+ ] = None,
32
+ open_conf: Annotated[
33
+ bool,
34
+ typer.Option("--conf", "-c", help="Open the .conf file instead of the keymap."),
35
+ ] = False,
36
+ open_build_matrix: Annotated[
37
+ bool,
38
+ typer.Option(
39
+ "--build", "-b", help="Open the build matrix instead of a keymap."
40
+ ),
41
+ ] = False,
42
+ ):
43
+ """Open the repo or a .keymap or .conf file in a text editor."""
44
+
45
+ cfg = ctx.find_object(Config)
46
+ repo = cfg.get_repo()
47
+
48
+ if open_build_matrix:
49
+ path = repo.build_matrix_path
50
+ else:
51
+ path = _get_file(repo, keyboard, open_conf)
52
+
53
+ editor = _get_editor(cfg, path.is_dir())
54
+
55
+ cmd = shlex.split(editor) + [path]
56
+ subprocess.call(cmd, shell=True)
57
+
58
+
59
+ def _get_file(repo: Repo, keyboard: str, open_conf: bool):
60
+ if not keyboard:
61
+ return repo.path
62
+
63
+ ext = ".conf" if open_conf else ".keymap"
64
+ return repo.config_path / f"{keyboard}{ext}"
65
+
66
+
67
+ def _get_editor(cfg: Config, is_directory: bool):
68
+ if is_directory:
69
+ try:
70
+ return cfg.get(Settings.CORE_EXPLORER)
71
+ except NoOptionError:
72
+ pass
73
+ try:
74
+ return cfg.get(Settings.CORE_EDITOR)
75
+ except NoOptionError:
76
+ pass
77
+
78
+ # No editor found. Prompt the user to select one and try again.
79
+ _select_editor(cfg)
80
+ return _get_editor(cfg, is_directory)
81
+
82
+
83
+ class Support(Flag):
84
+ """Types of files an editor supports"""
85
+
86
+ FILE = auto()
87
+ DIR = auto()
88
+
89
+ ALL = FILE | DIR
90
+
91
+
92
+ @dataclass
93
+ class Editor:
94
+ """File editing tool"""
95
+
96
+ name: str
97
+ cmd: str
98
+ "Executable name or command line to execute this tool"
99
+ support: Support = Support.FILE
100
+ "Types of files this tool supports editing"
101
+ test: Callable[[], bool] = None
102
+ """
103
+ Function that returns true if the tool is installed.
104
+ Defaults to `which {self.cmd}`.
105
+ """
106
+ paths: list[str] = field(default_factory=list)
107
+ "Extra paths to search for the executable if `test` is not set"
108
+
109
+ def __rich__(self):
110
+ return self.name
111
+
112
+ def get_command(self):
113
+ """Get the command to execute the tool, or None if it is not installed"""
114
+ if self.test and self.test():
115
+ return self.cmd
116
+
117
+ if shutil.which(self.cmd):
118
+ return self.cmd
119
+
120
+ for path in self.paths:
121
+ if cmd := shutil.which(self.cmd, path=path):
122
+ return shlex.quote(cmd)
123
+
124
+ return None
125
+
126
+
127
+ def _mac():
128
+ return platform.system() == "Darwin"
129
+
130
+
131
+ _NPP_PATHS = ["C:\\Program Files\\Notepad++", "C:\\Program Files (x86)\\Notepad++"]
132
+ _SUBL_PATHS = [
133
+ "C:\\Program Files\\sublime text 3",
134
+ "C:\\Program Files (x86)\\sublime text 3",
135
+ ]
136
+
137
+
138
+ _EDITORS: list[Editor] = [
139
+ # Graphical text editors
140
+ Editor("Visual Studio Code", cmd="code", support=Support.ALL),
141
+ Editor("Sublime Text", cmd="subl", support=Support.ALL, paths=_SUBL_PATHS),
142
+ Editor("Emacs", cmd="emacs", support=Support.ALL),
143
+ Editor("Gedit", cmd="gedit"),
144
+ Editor("Notepad", cmd="notepad"),
145
+ Editor("Notepad++", cmd="notepad++", paths=_NPP_PATHS),
146
+ Editor("TextEdit", cmd="open -a TextEdit", test=_mac),
147
+ Editor("TextMate", cmd="mate", support=Support.ALL),
148
+ # Terminal text editors
149
+ Editor("Nano", cmd="nano"),
150
+ Editor("Neovim", cmd="nvim", support=Support.ALL),
151
+ Editor("Vim", cmd="vim", support=Support.ALL),
152
+ # File explorers
153
+ Editor("File Explorer", cmd="explorer", support=Support.DIR),
154
+ Editor("Nautilus", cmd="nautilus", support=Support.DIR),
155
+ Editor("Finder", cmd="open", support=Support.DIR, test=_mac),
156
+ ]
157
+
158
+
159
+ def _select_editor(cfg: Config):
160
+ available = [e for e in _EDITORS if e.get_command()]
161
+ file_editors = [e for e in available if e.support & Support.FILE]
162
+ dir_editors = [e for e in available if e.support & Support.DIR]
163
+
164
+ if not file_editors:
165
+ raise FatalError(
166
+ "Could not find a known text editor.\n"
167
+ "Run \"zmk config core.editor='<command>'\" "
168
+ 'replacing "<command>" with the path to a text editor.'
169
+ )
170
+
171
+ editor = show_menu("Select a text editor:", file_editors, filter_func=_filter)
172
+ cfg.set(Settings.CORE_EDITOR, editor.get_command())
173
+
174
+ explorer = None
175
+ if editor.support & Support.DIR:
176
+ cfg.remove(Settings.CORE_EXPLORER)
177
+ elif dir_editors:
178
+ explorer = show_menu(
179
+ "This text editor only supports opening files.\n"
180
+ "Select another tool for opening directories:",
181
+ dir_editors,
182
+ filter_func=_filter,
183
+ )
184
+ cfg.set(Settings.CORE_EXPLORER, explorer.get_command())
185
+
186
+ cfg.write()
187
+
188
+ rich.print("Editor saved:")
189
+ rich.print(Markdown(f'`zmk config core.editor="{editor.get_command()}"`'))
190
+ if explorer:
191
+ rich.print(Markdown(f'`zmk config core.explorer="{explorer.get_command()}"`'))
192
+
193
+ rich.print()
194
+
195
+
196
+ def _filter(editor: Editor, text: str):
197
+ return text.casefold() in editor.name.casefold()
zmk/commands/config.py ADDED
@@ -0,0 +1,83 @@
1
+ """
2
+ "zmk config" command.
3
+ """
4
+
5
+ from typing import Annotated, Optional
6
+
7
+ import typer
8
+ from rich.console import Console
9
+
10
+ from .. import styles
11
+ from ..config import Config
12
+
13
+ console = Console(
14
+ highlighter=styles.KeyValueHighlighter(), theme=styles.KEY_VALUE_THEME
15
+ )
16
+
17
+
18
+ def _path_callback(ctx: typer.Context, value: bool):
19
+ if value:
20
+ cfg = ctx.find_object(Config)
21
+ print(cfg.path)
22
+ raise typer.Exit()
23
+
24
+
25
+ def config(
26
+ ctx: typer.Context,
27
+ name: Annotated[
28
+ Optional[str],
29
+ typer.Argument(
30
+ help="Setting name. Prints all setting values if omitted.",
31
+ ),
32
+ ] = None,
33
+ value: Annotated[
34
+ Optional[str],
35
+ typer.Argument(help="New setting value. Prints the current value if omitted."),
36
+ ] = None,
37
+ unset: Annotated[
38
+ bool,
39
+ typer.Option("--unset", "-u", help="Remove the setting with the given name."),
40
+ ] = False,
41
+ _: Annotated[
42
+ Optional[bool],
43
+ typer.Option(
44
+ "--path",
45
+ "-p",
46
+ help="Print the path to the ZMK CLI configuration file and exit.",
47
+ is_eager=True,
48
+ callback=_path_callback,
49
+ ),
50
+ ] = False,
51
+ ):
52
+ """Get and set ZMK CLI settings."""
53
+
54
+ cfg = ctx.find_object(Config)
55
+
56
+ if name is None:
57
+ _list_settings(cfg)
58
+ elif unset:
59
+ _unset_setting(cfg, name)
60
+ elif value:
61
+ _set_setting(cfg, name, value)
62
+ else:
63
+ _get_setting(cfg, name)
64
+
65
+
66
+ def _list_settings(cfg: Config):
67
+ for name, value in sorted(cfg.items()):
68
+ console.print(f"{name}={value}")
69
+
70
+
71
+ def _unset_setting(cfg: Config, name: str):
72
+ cfg.remove(name)
73
+ cfg.write()
74
+
75
+
76
+ def _set_setting(cfg: Config, name: str, value: str):
77
+ cfg.set(name, value)
78
+ cfg.write()
79
+
80
+
81
+ def _get_setting(cfg: Config, name: str):
82
+ if value := cfg.get(name, fallback=None):
83
+ console.print(value)
@@ -0,0 +1,26 @@
1
+ """
2
+ "zmk download" command.
3
+ """
4
+
5
+ import typer
6
+
7
+ from ..config import Config
8
+ from ..repo import Repo
9
+
10
+
11
+ def download(ctx: typer.Context):
12
+ """Open the web page to download firmware from GitHub."""
13
+
14
+ cfg = ctx.find_object(Config)
15
+ repo = cfg.get_repo()
16
+
17
+ actions_url = _get_actions_url(repo)
18
+
19
+ typer.launch(actions_url)
20
+
21
+
22
+ def _get_actions_url(repo: Repo):
23
+ remote = repo.git("remote", capture_output=True).strip()
24
+ remote_url = repo.git("remote", "get-url", remote, capture_output=True).strip()
25
+
26
+ return f"{remote_url}/actions/workflows/build.yml"