Skip to content

Commit 69a9ca3

Browse files
authored
Merge pull request #19 from MrClock8163/feature/tree-listing
Displaying file listing in tree view
2 parents 421c1e0 + ce448ac commit 69a9ca3

File tree

5 files changed

+198
-56
lines changed

5 files changed

+198
-56
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,13 @@ file.
55

66
The project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## Unreleased
9+
10+
### Changed
11+
12+
- Updated file listing to display results in a tree view
13+
- Reworked file listing to be able to run recursively to build full tree view
14+
815
## v0.3.0 (2025-08-01)
916

1017
### Added

docs/commands/files/list.rst

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,17 +19,15 @@ Paths
1919

2020
The most general way of file listing is to not specify a file type (defaulting
2121
to unknown), and giving the directory path. Such a path should use ``/`` as
22-
separators (contrary to other Windows conventions) and should end with a ``/``.
22+
separators (contrary to other Windows conventions) and might end with a ``/``.
2323

2424
If a special type of file is to be listed (e.g. database), then it is enough
2525
to specify the file type, the path can be left out.
2626

2727
.. note::
2828

29-
On newer instruments giving just the directory path might not be enough
30-
to list all files in the directory. It may be necessary to give the path
31-
in a glob-like pattern, with wildcards for the filenames (e.g. to to list
32-
all files in the Data folder, the path would be ``Data/*.*``)
29+
On older instruments, the file listing might not return directories. In
30+
these cases the recursive file tree cannot be created.
3331

3432
Examples
3533
--------
@@ -42,7 +40,12 @@ Examples
4240
.. code-block:: shell
4341
:caption: Listing all exported files on a CF card
4442
45-
iman list files -d cf COM1 Data/
43+
iman list files -d cf COM1 Data
44+
45+
.. code-block:: shell
46+
:caption: Listing all contents of a directory recursively
47+
48+
iman list files --depth 0 COM1 Data
4649
4750
Usage
4851
-----

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ requires-python = ">=3.11"
1414
dependencies = [
1515
"pyserial ~= 3.5.0",
1616
"geocompy >= 0.8.1",
17+
"rich >= 13.9",
1718
"textual >= 3.2.0",
1819
"rapidfuzz ~= 3.13.0",
19-
"jsonschema ~= 4.21.0",
20+
"jsonschema ~= 4.21",
2021
"jmespath ~= 1.0.1",
2122
"toml ~= 0.10.2",
2223
"PyYAML ~= 6.0.2",

src/instrumentman/filetransfer/__init__.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
@option(
4848
"-f",
4949
"--filetype",
50-
help="file type",
50+
help="file type (ignored in recursive mode)",
5151
type=Choice(
5252
(
5353
"image",
@@ -57,12 +57,19 @@
5757
"telescope-jpg",
5858
"telescope-bmp",
5959
"scan",
60-
"unknown",
6160
"last"
6261
),
6362
case_sensitive=False
63+
)
64+
)
65+
@option(
66+
"--depth",
67+
help=(
68+
"recursive depth "
69+
"(0: unlimited; 1<=x: depth of directory search)"
6470
),
65-
default="unknown"
71+
type=IntRange(0),
72+
default=1
6673
)
6774
def cli_list(**kwargs: Any) -> None:
6875
"""List files on an instrument."""

src/instrumentman/filetransfer/app.py

Lines changed: 170 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,23 @@
1+
from __future__ import annotations
2+
13
from io import BufferedWriter
4+
from typing import TypedDict, Generator
5+
import os
6+
from re import compile, IGNORECASE
27

8+
from click._termui_impl import ProgressBar
39
from click_extra import echo, progressbar
10+
from rich.console import Console, RenderableType
11+
from rich.text import Text
12+
from rich.tree import Tree
13+
from rich.table import Table
14+
from rich.filesize import decimal
415
from geocompy.communication import open_serial
516
from geocompy.geo import GeoCom
617
from geocompy.geo.gctypes import GeoComCode
718
from geocompy.geo.gcdata import File, Device
819

9-
from ..utils import echo_red, echo_green, echo_yellow
20+
from ..utils import echo_red, echo_green
1021

1122

1223
_FILE = {
@@ -31,74 +42,186 @@
3142
}
3243

3344

34-
def run_listing(
45+
class FileTreeItem(TypedDict):
46+
name: str
47+
size: int
48+
date: str
49+
children: list[FileTreeItem]
50+
51+
52+
def get_directory_items(
53+
bar: ProgressBar[str],
3554
tps: GeoCom,
36-
dev: str,
55+
device: str,
3756
directory: str,
38-
filetype: str
39-
) -> None:
57+
filetype: str,
58+
depth: int = 0
59+
) -> list[FileTreeItem]:
60+
if depth == 0:
61+
return []
62+
bar.update(1, directory)
4063
resp_setup = tps.ftr.setup_listing(
41-
_DEVICE[dev],
64+
_DEVICE[device],
4265
_FILE[filetype],
43-
directory
66+
f"{directory}/*"
4467
)
4568
if resp_setup.error != GeoComCode.OK:
46-
echo_red(f"Could not set up file listing ({resp_setup.error.name})")
47-
return
69+
return []
4870

4971
resp_list = tps.ftr.list()
5072
if resp_list.error != GeoComCode.OK or resp_list.params is None:
51-
echo_red(f"Could not start listing ({resp_list.error.name})")
52-
return
73+
tps.ftr.abort_list()
74+
return []
5375

5476
last, name, size, lastmodified = resp_list.params
5577
if name == "":
56-
echo_yellow("Directory is empty or path does not exist")
57-
return
78+
tps.ftr.abort_list()
79+
return []
80+
81+
output: list[FileTreeItem] = []
82+
output.append(
83+
{
84+
"name": name,
85+
"size": size,
86+
"date": (
87+
lastmodified.isoformat(sep=" ")
88+
if lastmodified is not None
89+
else ""
90+
),
91+
"children": []
92+
}
93+
)
94+
while not last:
95+
resp_list = tps.ftr.list(True)
96+
if resp_list.error != GeoComCode.OK or resp_list.params is None:
97+
tps.ftr.abort_list()
98+
return []
5899

59-
count = 1
60-
echo(f"{'file name':<55.55s}{'bytes':>10.10s}{'last modified':>25.25s}")
61-
echo(f"{'---------':<55.55s}{'-----':>10.10s}{'-------------':>25.25s}")
62-
fmt = "{name:<55.55s}{size:>10s}{date:>25.25s}"
63-
echo(
64-
fmt.format_map(
100+
last, name, size, lastmodified = resp_list.params
101+
output.append(
65102
{
66103
"name": name,
67-
"size": str(size),
104+
"size": size,
68105
"date": (
69106
lastmodified.isoformat(sep=" ")
70107
if lastmodified is not None
71108
else ""
72-
)
109+
),
110+
"children": []
73111
}
74112
)
113+
114+
tps.ftr.abort_list()
115+
for item in output:
116+
item["children"] = get_directory_items(
117+
bar,
118+
tps,
119+
device,
120+
f"{directory}/{item['name']}",
121+
filetype,
122+
depth=(depth - 1) if depth > 0 else -1
123+
)
124+
125+
return output
126+
127+
128+
_RE_DBX = compile(r"(?:.X\d{2})|.xcf", IGNORECASE)
129+
_fmt_dir = ":open_file_folder: [bold blue]{name}[/] [bright_black]({count})"
130+
_fmt_likelydir = ":grey_question: [cyan]{name}"
131+
_fmt_text = ":pencil: [green]{name}"
132+
_fmt_img = ":city_sunset: [bright_magenta]{name}"
133+
_fmt_dbx = ":package: [red]{name}"
134+
_fmt_unkown = ":grey_question: {name}"
135+
136+
137+
def format_tree_item(
138+
tree: FileTreeItem
139+
) -> RenderableType:
140+
141+
if len(tree["children"]) > 0:
142+
name = _fmt_dir.format(
143+
name=tree["name"],
144+
count=len(tree["children"])
145+
)
146+
else:
147+
match os.path.splitext(tree["name"])[1].lower():
148+
case "":
149+
name = _fmt_likelydir.format_map(tree)
150+
case ".jpg" | ".jpeg" | ".bmp" | ".dxf" | ".dwg":
151+
name = _fmt_img.format_map(tree)
152+
case ".txt" | ".gsi" | ".xml":
153+
name = _fmt_text.format_map(tree)
154+
case dbx if _RE_DBX.match(dbx):
155+
name = _fmt_dbx.format_map(tree)
156+
case _:
157+
name = _fmt_unkown.format_map(tree)
158+
159+
grid = Table.grid(expand=True)
160+
grid.add_column()
161+
grid.add_column(justify="right")
162+
grid.add_row(
163+
Text.from_markup(name),
164+
Text(
165+
f"{decimal(tree['size']):>10.10s}{tree['date']:>25.25s}",
166+
justify="right"
167+
)
75168
)
76-
while not last:
77-
resp_list = tps.ftr.list(True)
78-
if resp_list.error != GeoComCode.OK or resp_list.params is None:
79-
echo_red(
80-
f"An error occured during listing ({resp_list.error.name})"
81-
)
82-
return
169+
return grid
83170

84-
last, name, size, lastmodified = resp_list.params
85-
echo(
86-
fmt.format_map(
87-
{
88-
"name": name,
89-
"size": str(size),
90-
"date": (
91-
lastmodified.isoformat(sep=" ")
92-
if lastmodified is not None
93-
else ""
94-
)
95-
}
171+
172+
def build_file_tree(
173+
tree: FileTreeItem,
174+
branch: Tree | None = None
175+
) -> Tree:
176+
if branch is None:
177+
branch = Tree(format_tree_item(tree))
178+
179+
for item in tree["children"]:
180+
node = branch.add(format_tree_item(item))
181+
build_file_tree(item, node)
182+
183+
return branch
184+
185+
186+
def _infinite_iterator() -> Generator[str, None, None]:
187+
while True:
188+
yield ""
189+
190+
191+
def run_listing_tree(
192+
tps: GeoCom,
193+
dev: str,
194+
directory: str,
195+
filetype: str | None,
196+
depth: int = 1
197+
) -> None:
198+
with progressbar(
199+
_infinite_iterator(),
200+
label="Searching directories",
201+
item_show_func=lambda x: "" if x is None else str(x)
202+
) as bar:
203+
tree: FileTreeItem = {
204+
"name": (
205+
f"{dev.upper()}/{directory}"
206+
if directory != "/"
207+
else dev.upper()
208+
),
209+
"size": 0,
210+
"date": "unknown",
211+
"children": get_directory_items(
212+
bar,
213+
tps,
214+
dev,
215+
directory,
216+
filetype or "unknown",
217+
-1 if depth == 0 else depth
96218
)
97-
)
98-
count += 1
219+
}
220+
bar.finish()
99221

100-
echo("-" * 90)
101-
echo(f"total: {count} files")
222+
treeview = build_file_tree(tree)
223+
console = Console(width=120)
224+
console.print(treeview)
102225

103226

104227
def run_download(
@@ -180,7 +303,8 @@ def main_list(
180303
retry: int = 1,
181304
sync_after_timeout: bool = False,
182305
device: str = "internal",
183-
filetype: str = "unknown"
306+
filetype: str | None = None,
307+
depth: int = 1
184308
) -> None:
185309
with open_serial(
186310
port=port,
@@ -191,6 +315,6 @@ def main_list(
191315
) as com:
192316
tps = GeoCom(com)
193317
try:
194-
run_listing(tps, device, directory, filetype)
318+
run_listing_tree(tps, device, directory, filetype, depth)
195319
finally:
196320
tps.ftr.abort_list()

0 commit comments

Comments
 (0)