1+ from __future__ import annotations
2+
13from 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
39from 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
415from geocompy .communication import open_serial
516from geocompy .geo import GeoCom
617from geocompy .geo .gctypes import GeoComCode
718from 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 = {
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
104227def 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