kash-shell 0.3.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 (269) hide show
  1. kash/__init__.py +2 -0
  2. kash/__main__.py +4 -0
  3. kash/actions/__init__.py +55 -0
  4. kash/actions/core/assistant_chat.py +45 -0
  5. kash/actions/core/chat.py +90 -0
  6. kash/actions/core/format_markdown_template.py +92 -0
  7. kash/actions/core/markdownify.py +29 -0
  8. kash/actions/core/readability.py +27 -0
  9. kash/actions/core/show_webpage.py +28 -0
  10. kash/actions/core/strip_html.py +28 -0
  11. kash/actions/core/summarize_as_bullets.py +53 -0
  12. kash/actions/core/webpage_config.py +21 -0
  13. kash/actions/core/webpage_generate.py +29 -0
  14. kash/actions/meta/write_instructions.py +39 -0
  15. kash/actions/meta/write_new_action.py +157 -0
  16. kash/commands/__init__.py +21 -0
  17. kash/commands/base/basic_file_commands.py +183 -0
  18. kash/commands/base/browser_commands.py +62 -0
  19. kash/commands/base/debug_commands.py +214 -0
  20. kash/commands/base/diff_commands.py +90 -0
  21. kash/commands/base/files_command.py +408 -0
  22. kash/commands/base/general_commands.py +104 -0
  23. kash/commands/base/global_state_commands.py +41 -0
  24. kash/commands/base/logs_commands.py +92 -0
  25. kash/commands/base/reformat_command.py +54 -0
  26. kash/commands/base/search_command.py +65 -0
  27. kash/commands/base/show_command.py +69 -0
  28. kash/commands/extras/utils_commands.py +27 -0
  29. kash/commands/help/assistant_commands.py +97 -0
  30. kash/commands/help/doc_commands.py +226 -0
  31. kash/commands/help/help_commands.py +133 -0
  32. kash/commands/workspace/selection_commands.py +200 -0
  33. kash/commands/workspace/workspace_commands.py +640 -0
  34. kash/concepts/concept_formats.py +23 -0
  35. kash/concepts/embeddings.py +130 -0
  36. kash/concepts/text_similarity.py +108 -0
  37. kash/config/__init__.py +4 -0
  38. kash/config/api_keys.py +84 -0
  39. kash/config/capture_output.py +77 -0
  40. kash/config/colors.py +279 -0
  41. kash/config/init.py +18 -0
  42. kash/config/lazy_imports.py +22 -0
  43. kash/config/logger.py +355 -0
  44. kash/config/logger_basic.py +35 -0
  45. kash/config/logo.txt +4 -0
  46. kash/config/logo_fancy.txt +4 -0
  47. kash/config/server_config.py +51 -0
  48. kash/config/settings.py +196 -0
  49. kash/config/setup.py +51 -0
  50. kash/config/suppress_warnings.py +27 -0
  51. kash/config/text_styles.py +426 -0
  52. kash/docs/__init__.py +0 -0
  53. kash/docs/all_docs.py +58 -0
  54. kash/docs/load_actions_info.py +28 -0
  55. kash/docs/load_api_docs.py +13 -0
  56. kash/docs/load_help_topics.py +47 -0
  57. kash/docs/load_source_code.py +125 -0
  58. kash/docs/markdown/api_docs_template.md +42 -0
  59. kash/docs/markdown/assistant_instructions_template.md +114 -0
  60. kash/docs/markdown/readme_template.md +26 -0
  61. kash/docs/markdown/topics/a1_what_is_kash.md +76 -0
  62. kash/docs/markdown/topics/a2_progress.md +96 -0
  63. kash/docs/markdown/topics/a3_installation.md +119 -0
  64. kash/docs/markdown/topics/a4_getting_started.md +300 -0
  65. kash/docs/markdown/topics/a5_tips_for_use_with_other_tools.md +83 -0
  66. kash/docs/markdown/topics/b0_philosophy_of_kash.md +177 -0
  67. kash/docs/markdown/topics/b1_kash_overview.md +124 -0
  68. kash/docs/markdown/topics/b2_workspace_and_file_formats.md +61 -0
  69. kash/docs/markdown/topics/b3_modern_shell_tool_recommendations.md +83 -0
  70. kash/docs/markdown/topics/b4_faq.md +166 -0
  71. kash/docs/markdown/warning.md +7 -0
  72. kash/docs/markdown/welcome.md +9 -0
  73. kash/docs_base/docs_base.py +85 -0
  74. kash/docs_base/load_custom_command_info.py +27 -0
  75. kash/docs_base/load_faqs.py +48 -0
  76. kash/docs_base/load_recipe_snippets.py +48 -0
  77. kash/docs_base/recipes/general_system_commands.ksh +10 -0
  78. kash/docs_base/recipes/python_dev_commands.ksh +7 -0
  79. kash/docs_base/recipes/tldr_standard_commands.ksh +2144 -0
  80. kash/errors.py +176 -0
  81. kash/exec/__init__.py +16 -0
  82. kash/exec/action_decorators.py +412 -0
  83. kash/exec/action_exec.py +457 -0
  84. kash/exec/action_registry.py +123 -0
  85. kash/exec/combiners.py +127 -0
  86. kash/exec/command_exec.py +34 -0
  87. kash/exec/command_registry.py +72 -0
  88. kash/exec/fetch_url_metadata.py +71 -0
  89. kash/exec/history.py +44 -0
  90. kash/exec/llm_transforms.py +121 -0
  91. kash/exec/precondition_checks.py +71 -0
  92. kash/exec/precondition_registry.py +43 -0
  93. kash/exec/preconditions.py +152 -0
  94. kash/exec/resolve_args.py +123 -0
  95. kash/exec/shell_callable_action.py +90 -0
  96. kash/exec_model/__init__.py +0 -0
  97. kash/exec_model/args_model.py +93 -0
  98. kash/exec_model/commands_model.py +163 -0
  99. kash/exec_model/script_model.py +161 -0
  100. kash/exec_model/shell_model.py +21 -0
  101. kash/file_storage/__init__.py +0 -0
  102. kash/file_storage/file_store.py +642 -0
  103. kash/file_storage/item_file_format.py +152 -0
  104. kash/file_storage/metadata_dirs.py +108 -0
  105. kash/file_storage/mtime_cache.py +108 -0
  106. kash/file_storage/persisted_yaml.py +37 -0
  107. kash/file_storage/store_cache_warmer.py +37 -0
  108. kash/file_storage/store_filenames.py +53 -0
  109. kash/form_input/__init__.py +0 -0
  110. kash/form_input/prompt_input.py +44 -0
  111. kash/help/__init__.py +0 -0
  112. kash/help/assistant.py +324 -0
  113. kash/help/assistant_instructions.py +68 -0
  114. kash/help/assistant_output.py +43 -0
  115. kash/help/docstring_utils.py +111 -0
  116. kash/help/function_param_info.py +44 -0
  117. kash/help/help_embeddings.py +85 -0
  118. kash/help/help_lookups.py +60 -0
  119. kash/help/help_pages.py +122 -0
  120. kash/help/help_printing.py +169 -0
  121. kash/help/help_types.py +247 -0
  122. kash/help/recommended_commands.py +143 -0
  123. kash/help/tldr_help.py +296 -0
  124. kash/llm_utils/__init__.py +0 -0
  125. kash/llm_utils/chat_format.py +413 -0
  126. kash/llm_utils/clean_headings.py +65 -0
  127. kash/llm_utils/fuzzy_parsing.py +119 -0
  128. kash/llm_utils/language_models.py +178 -0
  129. kash/llm_utils/llm_completion.py +172 -0
  130. kash/llm_utils/llm_messages.py +36 -0
  131. kash/local_server/__init__.py +2 -0
  132. kash/local_server/local_server.py +183 -0
  133. kash/local_server/local_server_commands.py +55 -0
  134. kash/local_server/local_server_routes.py +306 -0
  135. kash/local_server/local_url_formatters.py +169 -0
  136. kash/local_server/port_tools.py +67 -0
  137. kash/local_server/rich_html_template.py +12 -0
  138. kash/mcp/__init__.py +2 -0
  139. kash/mcp/mcp_main.py +67 -0
  140. kash/mcp/mcp_server_commands.py +57 -0
  141. kash/mcp/mcp_server_routes.py +256 -0
  142. kash/mcp/mcp_server_sse.py +143 -0
  143. kash/mcp/mcp_server_stdio.py +45 -0
  144. kash/media_base/__init__.py +0 -0
  145. kash/media_base/audio_processing.py +27 -0
  146. kash/media_base/media_cache.py +178 -0
  147. kash/media_base/media_services.py +112 -0
  148. kash/media_base/media_tools.py +48 -0
  149. kash/media_base/services/local_file_media.py +165 -0
  150. kash/media_base/speech_transcription.py +224 -0
  151. kash/media_base/timestamp_citations.py +80 -0
  152. kash/model/__init__.py +73 -0
  153. kash/model/actions_model.py +633 -0
  154. kash/model/assistant_response_model.py +87 -0
  155. kash/model/compound_actions_model.py +188 -0
  156. kash/model/graph_model.py +92 -0
  157. kash/model/items_model.py +821 -0
  158. kash/model/language_list.py +39 -0
  159. kash/model/llm_actions_model.py +63 -0
  160. kash/model/media_model.py +124 -0
  161. kash/model/operations_model.py +176 -0
  162. kash/model/params_model.py +435 -0
  163. kash/model/paths_model.py +458 -0
  164. kash/model/preconditions_model.py +98 -0
  165. kash/shell/__init__.py +0 -0
  166. kash/shell/completions/completion_scoring.py +280 -0
  167. kash/shell/completions/completion_types.py +154 -0
  168. kash/shell/completions/shell_completions.py +277 -0
  169. kash/shell/file_icons/color_for_format.py +70 -0
  170. kash/shell/file_icons/nerd_icons.py +946 -0
  171. kash/shell/output/__init__.py +0 -0
  172. kash/shell/output/kerm_code_utils.py +59 -0
  173. kash/shell/output/kerm_codes.py +588 -0
  174. kash/shell/output/kmarkdown.py +117 -0
  175. kash/shell/output/shell_output.py +477 -0
  176. kash/shell/ui/__init__.py +0 -0
  177. kash/shell/ui/shell_results.py +118 -0
  178. kash/shell/ui/shell_syntax.py +26 -0
  179. kash/shell/utils/exception_printing.py +50 -0
  180. kash/shell/utils/native_utils.py +240 -0
  181. kash/shell/utils/osc_utils.py +95 -0
  182. kash/shell/utils/shell_function_wrapper.py +204 -0
  183. kash/shell/utils/sys_tool_deps.py +289 -0
  184. kash/shell/utils/terminal_images.py +133 -0
  185. kash/shell_main.py +67 -0
  186. kash/text_handling/custom_sliding_transforms.py +266 -0
  187. kash/text_handling/doc_normalization.py +64 -0
  188. kash/text_handling/markdown_util.py +167 -0
  189. kash/text_handling/unified_diffs.py +138 -0
  190. kash/utils/__init__.py +4 -0
  191. kash/utils/common/__init__.py +4 -0
  192. kash/utils/common/atomic_var.py +147 -0
  193. kash/utils/common/format_utils.py +81 -0
  194. kash/utils/common/function_inspect.py +178 -0
  195. kash/utils/common/import_utils.py +89 -0
  196. kash/utils/common/lazyobject.py +144 -0
  197. kash/utils/common/obj_replace.py +78 -0
  198. kash/utils/common/parse_key_vals.py +85 -0
  199. kash/utils/common/parse_shell_args.py +348 -0
  200. kash/utils/common/stack_traces.py +49 -0
  201. kash/utils/common/string_replace.py +93 -0
  202. kash/utils/common/string_template.py +101 -0
  203. kash/utils/common/task_stack.py +162 -0
  204. kash/utils/common/type_utils.py +137 -0
  205. kash/utils/common/uniquifier.py +95 -0
  206. kash/utils/common/url.py +155 -0
  207. kash/utils/file_utils/__init__.py +3 -0
  208. kash/utils/file_utils/dir_size.py +48 -0
  209. kash/utils/file_utils/file_ext.py +86 -0
  210. kash/utils/file_utils/file_formats.py +134 -0
  211. kash/utils/file_utils/file_formats_model.py +408 -0
  212. kash/utils/file_utils/file_sort_filter.py +235 -0
  213. kash/utils/file_utils/file_walk.py +163 -0
  214. kash/utils/file_utils/filename_parsing.py +99 -0
  215. kash/utils/file_utils/git_tools.py +19 -0
  216. kash/utils/file_utils/ignore_files.py +166 -0
  217. kash/utils/file_utils/path_utils.py +36 -0
  218. kash/utils/lang_utils/__init__.py +0 -0
  219. kash/utils/lang_utils/capitalization.py +128 -0
  220. kash/utils/lang_utils/inflection.py +18 -0
  221. kash/utils/rich_custom/__init__.py +3 -0
  222. kash/utils/rich_custom/ansi_cell_len.py +72 -0
  223. kash/utils/rich_custom/rich_char_transform.py +89 -0
  224. kash/utils/rich_custom/rich_indent.py +69 -0
  225. kash/utils/rich_custom/rich_markdown_fork.py +771 -0
  226. kash/version.py +31 -0
  227. kash/web_content/canon_url.py +24 -0
  228. kash/web_content/dir_store.py +103 -0
  229. kash/web_content/file_cache_utils.py +117 -0
  230. kash/web_content/local_file_cache.py +247 -0
  231. kash/web_content/web_extract.py +55 -0
  232. kash/web_content/web_extract_justext.py +86 -0
  233. kash/web_content/web_extract_readabilipy.py +23 -0
  234. kash/web_content/web_fetch.py +101 -0
  235. kash/web_content/web_page_model.py +28 -0
  236. kash/web_gen/__init__.py +4 -0
  237. kash/web_gen/tabbed_webpage.py +149 -0
  238. kash/web_gen/template_render.py +29 -0
  239. kash/web_gen/templates/base_styles.css.jinja +192 -0
  240. kash/web_gen/templates/base_webpage.html.jinja +124 -0
  241. kash/web_gen/templates/content_styles.css.jinja +194 -0
  242. kash/web_gen/templates/explain_view.html.jinja +49 -0
  243. kash/web_gen/templates/item_view.html.jinja +294 -0
  244. kash/web_gen/templates/tabbed_webpage.html.jinja +49 -0
  245. kash/workspaces/__init__.py +13 -0
  246. kash/workspaces/param_state.py +24 -0
  247. kash/workspaces/selections.py +333 -0
  248. kash/workspaces/source_items.py +88 -0
  249. kash/workspaces/workspace_importing.py +56 -0
  250. kash/workspaces/workspace_names.py +33 -0
  251. kash/workspaces/workspace_output.py +154 -0
  252. kash/workspaces/workspace_registry.py +78 -0
  253. kash/workspaces/workspaces.py +197 -0
  254. kash/xonsh_custom/custom_shell.py +366 -0
  255. kash/xonsh_custom/customize_prompt.py +197 -0
  256. kash/xonsh_custom/customize_xonsh.py +112 -0
  257. kash/xonsh_custom/shell_load_commands.py +152 -0
  258. kash/xonsh_custom/shell_which.py +64 -0
  259. kash/xonsh_custom/xonsh_completers.py +715 -0
  260. kash/xonsh_custom/xonsh_env.py +28 -0
  261. kash/xonsh_custom/xonsh_modern_tools.py +56 -0
  262. kash/xonsh_custom/xonsh_ranking_completer.py +152 -0
  263. kash/xontrib/fnm.py +120 -0
  264. kash/xontrib/kash_extension.py +61 -0
  265. kash_shell-0.3.0.dist-info/METADATA +757 -0
  266. kash_shell-0.3.0.dist-info/RECORD +269 -0
  267. kash_shell-0.3.0.dist-info/WHEEL +4 -0
  268. kash_shell-0.3.0.dist-info/entry_points.txt +3 -0
  269. kash_shell-0.3.0.dist-info/licenses/LICENSE +664 -0
@@ -0,0 +1,90 @@
1
+ from pathlib import Path
2
+
3
+ from kash.commands.base.show_command import show
4
+ from kash.commands.workspace.selection_commands import select
5
+ from kash.config.logger import get_logger
6
+ from kash.errors import InvalidInput, InvalidOperation
7
+ from kash.exec import import_locator_args, kash_command
8
+ from kash.exec_model.shell_model import ShellResult
9
+ from kash.model.items_model import Item, ItemType
10
+ from kash.shell.output.shell_output import Wrap, cprint
11
+ from kash.text_handling.unified_diffs import unified_diff_files, unified_diff_items
12
+ from kash.utils.file_utils.file_formats_model import Format
13
+ from kash.workspaces import current_ws
14
+
15
+ log = get_logger(__name__)
16
+
17
+
18
+ @kash_command
19
+ def diff_items(*paths: str, force: bool = False) -> ShellResult:
20
+ """
21
+ Show the unified diff between the given files. It's often helpful to treat diffs
22
+ as items themselves, so this works on items. Items are imported as usual into the
23
+ global workspace if they are not already in the store.
24
+
25
+ :param stat: Only show the diffstat summary.
26
+ :param force: If true, will run the diff even if the items are of different formats.
27
+ """
28
+ ws = current_ws()
29
+ if len(paths) == 2:
30
+ [path1, path2] = paths
31
+ elif len(paths) == 0:
32
+ try:
33
+ last_selections = ws.selections.previous_n(2, expected_size=1)
34
+ except InvalidOperation:
35
+ raise InvalidInput(
36
+ "Need two selections of single files in history or exactly two paths to diff"
37
+ )
38
+ [path1] = last_selections[0].paths
39
+ [path2] = last_selections[1].paths
40
+ else:
41
+ raise InvalidInput("Provide zero paths (to use selections) or two paths to diff")
42
+
43
+ [store_path1, store_path2] = import_locator_args(path1, path2)
44
+ item1, item2 = ws.load(store_path1), ws.load(store_path2)
45
+
46
+ diff_item = unified_diff_items(item1, item2, strict=not force)
47
+ diff_store_path = ws.save(diff_item, as_tmp=False)
48
+ select(diff_store_path)
49
+ return ShellResult(show_selection=True)
50
+
51
+
52
+ @kash_command
53
+ def diff_files(*paths: str, diffstat: bool = False, save: bool = False) -> ShellResult:
54
+ """
55
+ Show the unified diff between the given files. This works on any files, not
56
+ just items, so helpful for quick analysis without importing the files.
57
+
58
+ :param diffstat: Only show the diffstat summary.
59
+ :param save: Save the diff as an item in the store.
60
+ """
61
+ if len(paths) == 2:
62
+ [path1, path2] = paths
63
+ elif len(paths) == 0:
64
+ # If nothing args given, user probably wants diff_items on selections.
65
+ return diff_items()
66
+ else:
67
+ raise InvalidInput("Provide zero paths (to use selections) or two paths to diff")
68
+
69
+ path1, path2 = Path(path1), Path(path2)
70
+ diff = unified_diff_files(path1, path2)
71
+
72
+ if diffstat:
73
+ cprint(diff.diffstat, text_wrap=Wrap.NONE)
74
+ return ShellResult(show_selection=False)
75
+ else:
76
+ diff_item = Item(
77
+ type=ItemType.doc,
78
+ title=f"Diff of {path1.name} and {path2.name}",
79
+ format=Format.diff,
80
+ body=diff.patch_text,
81
+ )
82
+ ws = current_ws()
83
+ if save:
84
+ diff_store_path = ws.save(diff_item, as_tmp=False)
85
+ select(diff_store_path)
86
+ return ShellResult(show_selection=True)
87
+ else:
88
+ diff_store_path = ws.save(diff_item, as_tmp=True)
89
+ show(diff_store_path)
90
+ return ShellResult(show_selection=False)
@@ -0,0 +1,408 @@
1
+ from datetime import UTC, datetime
2
+ from pathlib import Path
3
+ from typing import cast
4
+
5
+ from prettyfmt import fmt_path, fmt_size_human, fmt_time
6
+ from rich.text import Text
7
+
8
+ from kash.commands.workspace.selection_commands import select
9
+ from kash.config.logger import get_logger
10
+ from kash.config.settings import global_settings
11
+ from kash.config.text_styles import COLOR_EXTRA, EMOJI_WARN, STYLE_EMPH, STYLE_HINT, colorize_qty
12
+ from kash.exec import kash_command
13
+ from kash.exec_model.shell_model import ShellResult
14
+ from kash.local_server.local_url_formatters import local_url_formatter
15
+ from kash.model.items_model import Item, ItemType
16
+ from kash.model.paths_model import StorePath, parse_path_spec
17
+ from kash.shell.file_icons.color_for_format import color_for_format
18
+ from kash.shell.output.shell_output import PrintHooks, Wrap, console_pager, cprint
19
+ from kash.utils.file_utils.file_formats_model import Format, guess_format_by_name
20
+ from kash.utils.file_utils.file_sort_filter import (
21
+ FileInfo,
22
+ FileListing,
23
+ FileType,
24
+ GroupByOption,
25
+ SortOption,
26
+ collect_files,
27
+ parse_since,
28
+ type_suffix,
29
+ )
30
+ from kash.utils.file_utils.ignore_files import ignore_none
31
+ from kash.utils.file_utils.path_utils import common_parent_dir
32
+ from kash.workspaces import current_ignore, current_ws
33
+
34
+ log = get_logger(__name__)
35
+
36
+
37
+ def _print_listing_tallies(
38
+ file_listing: FileListing,
39
+ total_displayed: int,
40
+ total_displayed_size: int,
41
+ max_files: int,
42
+ max_depth: int,
43
+ max_per_subdir: int,
44
+ ) -> None:
45
+ if total_displayed > 0:
46
+ cprint(
47
+ f"{total_displayed} files ({fmt_size_human(total_displayed_size)}) shown",
48
+ style=COLOR_EXTRA,
49
+ )
50
+ if file_listing.files_total > file_listing.files_matching > total_displayed:
51
+ cprint(
52
+ f"of {file_listing.files_matching} files "
53
+ f"({fmt_size_human(file_listing.size_matching)}) matching criteria",
54
+ style=COLOR_EXTRA,
55
+ )
56
+ if file_listing.files_total > total_displayed:
57
+ cprint(
58
+ f"from {file_listing.files_total} total files "
59
+ f"({fmt_size_human(file_listing.size_total)})",
60
+ style=COLOR_EXTRA,
61
+ )
62
+ if file_listing.total_ignored > 0:
63
+ cprint(
64
+ f"{EMOJI_WARN} {file_listing.files_ignored} files and {file_listing.dirs_ignored} dirs were ignored",
65
+ style=COLOR_EXTRA,
66
+ )
67
+ cprint("(use --no_ignore to show hidden files)", style=STYLE_HINT)
68
+
69
+ if file_listing.total_skipped > 0:
70
+ cprint(
71
+ f"{EMOJI_WARN} long file listing: capped "
72
+ f"at max_files={max_files}, max_depth={max_depth}, max_per_subdir={max_per_subdir}",
73
+ style=COLOR_EXTRA,
74
+ )
75
+ cprint("(use --no_max to remove cutoff)", style=STYLE_HINT)
76
+
77
+
78
+ @kash_command
79
+ def files(
80
+ *paths: str,
81
+ overview: bool = False,
82
+ recent: bool = False,
83
+ recursive: bool = False,
84
+ flat: bool = False,
85
+ pager: bool = False,
86
+ omit_dirs: bool = False,
87
+ max_per_group: int = -1,
88
+ depth: int | None = None,
89
+ max_per_subdir: int = 1000,
90
+ max_files: int = 1000,
91
+ no_max: bool = False,
92
+ no_ignore: bool = False,
93
+ all: bool = False,
94
+ save: bool = False,
95
+ sort: SortOption | None = None,
96
+ reverse: bool = False,
97
+ since: str | None = None,
98
+ groupby: GroupByOption | None = GroupByOption.parent,
99
+ iso_time: bool = False,
100
+ ) -> ShellResult:
101
+ """
102
+ List files or folders in the current directory or specified paths.
103
+
104
+ Attempts to be similar to `ls` or `eza` but without any legacy constraints.
105
+
106
+ Aims for simple output, optional paging, a better "overview" mode that recurses
107
+ with limited depth and breadth, and more control over recursion, sorting,
108
+ and grouping.
109
+
110
+ :param overview: Recurse a couple levels and show files, but not too many.
111
+ Same as `--groupby=parent --depth=2 --max_per_group=10 --omit_dirs`.
112
+ :param recent: Only shows the most recently modified files in each directory.
113
+ Same as `--sort=modified --reverse --groupby=parent --max_per_group=10`.
114
+ :param recursive: List all files recursively. Same as `--depth=-1`.
115
+ :param flat: Show files in a flat list, rather than grouped by parent directory.
116
+ Same as `--groupby=flat`.
117
+ :param omit_dirs: Normally directories are included. This option omits them,
118
+ which is useful when recursing into subdirectories.
119
+ :param depth: Maximum depth to recurse into directories. -1 means no limit.
120
+ :param max_files: Maximum number of files to yield per input path.
121
+ -1 means no limit.
122
+ :param max_per_subdir: Maximum number of files to yield per subdirectory
123
+ (not including the top level). -1 means no limit.
124
+ :param max_per_group: Limit the first number of items displayed per group
125
+ (if groupby is used) or in total. 0 means show all.
126
+ :param no_max: Disable limits on depth and number of files. Same as
127
+ `--depth=-1 --max_files=-1 --max_per_subdir=-1 --max_per_group=-1`.
128
+ :param no_ignore: Disable ignoring hidden files.
129
+ :param all: Same as `--no_ignore --no_max`. Does not change `--depth`.
130
+ :param save: Save the listing as a CSV file item.
131
+ :param sort: Sort by `filename`, `size`, `accessed`, `created`, or `modified`.
132
+ :param reverse: Reverse the sorting order.
133
+ :param since: Filter files modified since a given time (e.g., '1 day', '2 hours').
134
+ :param groupby: Group results. Can be `flat` (no grouping, and by default implies
135
+ recursive), `parent`, or `suffix`. Defaults to 'parent'.
136
+ :param iso_time: Show time in ISO format (default is human-readable age).
137
+ :param pager: Use the pager when displaying the output.
138
+ """
139
+ if global_settings().use_nerd_icons:
140
+ from kash.shell.file_icons.nerd_icons import icon_for_file
141
+ else:
142
+ icon_for_file = None
143
+
144
+ # TODO: Add a --full option with line and word counts and file_info details
145
+ # and also include these in --save.
146
+
147
+ if len(paths) == 0:
148
+ paths_to_show = [Path(".")]
149
+ no_explicit_paths = True
150
+ else:
151
+ paths_to_show = [parse_path_spec(path) for path in paths]
152
+ no_explicit_paths = False
153
+
154
+ # Set up base path. If we have a workspace and if this listing is within the
155
+ # current workspace, detect that, since it's convenient to enable brief listings
156
+ # in workspaces.
157
+ cwd = Path.cwd()
158
+ ws = current_ws()
159
+ active_ws_name = ws.name if cwd.is_relative_to(ws.base_dir.resolve()) else None
160
+ # Check if all requested paths are within the current directory, and if so use
161
+ # that as the base path. Otherwise, use the common parent directory of all paths.
162
+ if paths_to_show:
163
+ base_path = common_parent_dir(*paths_to_show)
164
+ if base_path.is_relative_to(cwd):
165
+ base_path = cwd
166
+ within_cwd = base_path.is_relative_to(cwd)
167
+ else:
168
+ base_path = Path(".")
169
+ within_cwd = True
170
+ # Should we show absolute paths?
171
+ show_absolute_paths = not within_cwd
172
+ # Is this a listing of the current workspace?
173
+ is_ws_listing = active_ws_name and ws.base_dir.resolve() == base_path.resolve()
174
+
175
+ # Handle lots of different options.
176
+ if recursive:
177
+ depth = -1
178
+ if is_ws_listing and no_explicit_paths:
179
+ # Within workspaces, we show more files by default since they are always in
180
+ # subdirectories.
181
+ overview = True # Handled next.
182
+ if overview:
183
+ max_per_group = 10 if max_per_group <= 0 else max_per_group
184
+ groupby = GroupByOption.parent if groupby is None else groupby
185
+ depth = 2 if depth is None else depth
186
+ omit_dirs = True
187
+ if recent:
188
+ max_per_group = 10 if max_per_group <= 0 else max_per_group
189
+ groupby = GroupByOption.parent if groupby is None else groupby
190
+ depth = 2 if depth is None else depth
191
+ sort = SortOption.modified if sort is None else sort
192
+ reverse = True
193
+ if flat:
194
+ groupby = GroupByOption.flat
195
+ if all:
196
+ no_ignore = True
197
+ no_max = True
198
+ if no_max:
199
+ depth = max_per_subdir = max_per_group = max_files = -1
200
+ # Unless depth is specified, flat implies recursive (depth -1).
201
+ if groupby == GroupByOption.flat and depth is None:
202
+ depth = -1
203
+
204
+ # Default depth unless otherwise set or implied is 0 (like ls).
205
+ depth = 0 if depth is None else depth
206
+
207
+ since_seconds = parse_since(since) if since else 0.0
208
+
209
+ # Determine whether to show hidden files for this path.
210
+ is_ignored = current_ignore()
211
+ if no_ignore:
212
+ is_ignored = ignore_none
213
+ else:
214
+ for path in paths_to_show:
215
+ # log.info("Checking ignore for %s against filter %s", fmt_path(path), is_ignored)
216
+ if not no_ignore and is_ignored(path, is_dir=path.is_dir()):
217
+ log.info(
218
+ "Requested path is on the ignore list so disabling ignore: %s",
219
+ fmt_path(path),
220
+ )
221
+ is_ignored = ignore_none
222
+ break
223
+
224
+ # Collect all the files.
225
+ log.debug(
226
+ "Collecting files: %s",
227
+ {
228
+ "paths_to_show": paths_to_show,
229
+ "depth": depth,
230
+ "max_per_subdir": max_per_subdir,
231
+ "max_per_group": max_per_group,
232
+ "max_files": max_files,
233
+ "omit_dirs": omit_dirs,
234
+ "since_seconds": since_seconds,
235
+ "base_path": base_path,
236
+ "include_dirs": not omit_dirs,
237
+ },
238
+ )
239
+ file_listing = collect_files(
240
+ start_paths=paths_to_show,
241
+ ignore=is_ignored,
242
+ since_seconds=since_seconds,
243
+ max_depth=depth,
244
+ max_files_per_subdir=max_per_subdir,
245
+ max_files_total=max_files,
246
+ base_path=base_path,
247
+ include_dirs=not omit_dirs,
248
+ resolve_parent=show_absolute_paths,
249
+ )
250
+
251
+ log.info("Collected %s files.", file_listing.files_total)
252
+
253
+ if not file_listing.files:
254
+ cprint("No files found.")
255
+ PrintHooks.spacer()
256
+ _print_listing_tallies(file_listing, 0, 0, max_files, depth, max_per_subdir)
257
+ return ShellResult()
258
+
259
+ df = file_listing.as_dataframe()
260
+
261
+ if sort:
262
+ # Determine the primary and secondary sort columns.
263
+ primary_sort = sort.value
264
+ secondary_sort = "filename" if primary_sort != "filename" else "created"
265
+
266
+ df.sort_values(
267
+ by=[primary_sort, secondary_sort], ascending=[not reverse, True], inplace=True
268
+ )
269
+
270
+ items_matching = len(df)
271
+ log.info(f"Total items collected: {items_matching}")
272
+
273
+ if groupby and groupby != GroupByOption.flat:
274
+ grouped = df.groupby(groupby.value)
275
+ else:
276
+ grouped = [(None, df)]
277
+
278
+ if save:
279
+ item = Item(
280
+ type=ItemType.export,
281
+ title="File Listing",
282
+ description=f"Files in {', '.join(fmt_path(p) for p in paths_to_show)}",
283
+ format=Format.csv,
284
+ body=df.to_csv(index=False),
285
+ )
286
+ ws = current_ws()
287
+ store_path = ws.save(item, as_tmp=False)
288
+ log.message("File listing saved to: %s", fmt_path(store_path))
289
+
290
+ select(store_path)
291
+
292
+ return ShellResult(show_selection=True)
293
+
294
+ total_displayed = 0
295
+ total_displayed_size = 0
296
+ now = datetime.now(UTC)
297
+
298
+ # Define spacing constants.
299
+ TIME_WIDTH = 12
300
+ SIZE_WIDTH = 8
301
+ SPACING = " "
302
+ indent = " " * (TIME_WIDTH + SIZE_WIDTH + len(SPACING) * 2)
303
+
304
+ with console_pager(use_pager=pager):
305
+ with local_url_formatter(active_ws_name) as fmt:
306
+ for group_name, group_df in grouped:
307
+ # If items are grouped e.g. by parent directory, show the group name first.
308
+ if group_name:
309
+ cprint(
310
+ f"{group_name} ({len(group_df)} files)",
311
+ style=STYLE_EMPH,
312
+ text_wrap=Wrap.NONE,
313
+ )
314
+
315
+ if max_per_group > 0:
316
+ display_df = group_df.head(max_per_group)
317
+ else:
318
+ display_df = group_df
319
+
320
+ for row in display_df.itertuples(index=False, name="FileInfo"):
321
+ row = cast(FileInfo, row) # pyright: ignore
322
+ short_file_size = fmt_size_human(row.size)
323
+ full_file_size = f"{row.size} bytes"
324
+ short_mod_time = fmt_time(row.modified, iso_time=iso_time, now=now, brief=True)
325
+ full_mod_time = fmt_time(row.modified, friendly=True, now=now)
326
+ is_dir = row.type == FileType.dir
327
+
328
+ rel_path = str(row.relative_path)
329
+
330
+ # If we are listing from within a workspace and we are at the base
331
+ # of the workspace, we include the paths as store paths (with an @
332
+ # prefix). Otherwise, use regular paths.
333
+ if is_ws_listing:
334
+ display_path = StorePath(rel_path) # Add a local server link.
335
+ display_path_str = f"{display_path}{type_suffix(row)}"
336
+ else:
337
+ display_path = Path(rel_path)
338
+ display_path_str = f"{display_path}{type_suffix(row)}"
339
+
340
+ # Assemble output line.
341
+ line: list[str | Text] = []
342
+ line.append(
343
+ colorize_qty(
344
+ fmt.tooltip_link(
345
+ short_mod_time.rjust(TIME_WIDTH), tooltip=full_mod_time
346
+ )
347
+ )
348
+ )
349
+ line.append(SPACING)
350
+ if is_dir:
351
+ # TODO: Insert tallies of files/total size in a fast/efficient way.
352
+ line.append(" " * SIZE_WIDTH)
353
+ else:
354
+ line.append(
355
+ colorize_qty(
356
+ fmt.tooltip_link(
357
+ short_file_size.rjust(SIZE_WIDTH), tooltip=full_file_size
358
+ )
359
+ )
360
+ )
361
+ line.append(SPACING)
362
+ if icon_for_file:
363
+ icon = icon_for_file(rel_path, is_dir=is_dir)
364
+ color = color_for_format(guess_format_by_name(rel_path))
365
+ line.append(
366
+ fmt.tooltip_link(icon.icon_char, tooltip=icon.readable, style=color)
367
+ )
368
+ line.append(" ")
369
+ line.append(
370
+ fmt.path_link(
371
+ display_path,
372
+ link_text=display_path_str,
373
+ ),
374
+ )
375
+
376
+ cprint(Text.assemble(*line), text_wrap=Wrap.NONE)
377
+ total_displayed += 1
378
+ total_displayed_size += row.size
379
+
380
+ # Indicate if items are omitted.
381
+ if groupby and max_per_group > 0 and len(group_df) > max_per_group:
382
+ cprint(
383
+ f"{indent}… and {len(group_df) - max_per_group} more files",
384
+ style=COLOR_EXTRA,
385
+ text_wrap=Wrap.NONE,
386
+ )
387
+
388
+ if group_name:
389
+ PrintHooks.spacer()
390
+
391
+ if not groupby and max_per_group > 0 and items_matching > max_per_group:
392
+ cprint(
393
+ f"{indent}… and {items_matching - max_per_group} more files",
394
+ style=COLOR_EXTRA,
395
+ text_wrap=Wrap.NONE,
396
+ )
397
+
398
+ PrintHooks.spacer()
399
+ _print_listing_tallies(
400
+ file_listing,
401
+ total_displayed,
402
+ total_displayed_size,
403
+ max_files,
404
+ depth,
405
+ max_per_subdir,
406
+ )
407
+
408
+ return ShellResult()
@@ -0,0 +1,104 @@
1
+ from kash.config.api_keys import print_api_key_setup, warn_if_missing_api_keys
2
+ from kash.config.logger import get_logger
3
+ from kash.docs.all_docs import all_docs
4
+ from kash.exec import kash_command
5
+ from kash.help.tldr_help import tldr_refresh_cache
6
+ from kash.shell.output.shell_output import cprint, format_name_and_value
7
+ from kash.shell.utils.sys_tool_deps import sys_tool_check, terminal_feature_check
8
+
9
+ log = get_logger(__name__)
10
+
11
+
12
+ @kash_command
13
+ def version() -> None:
14
+ """
15
+ Show the version of kash.
16
+ """
17
+ from kash.shell_main import APP_VERSION
18
+
19
+ cprint(APP_VERSION)
20
+
21
+
22
+ @kash_command
23
+ def self_check(brief: bool = False) -> None:
24
+ """
25
+ Self-check kash setup, including termal settings, tools, and API keys.
26
+ """
27
+ if brief:
28
+ terminal_feature_check().print_term_info()
29
+ print_api_key_setup(once=False)
30
+ check_tools(brief=brief)
31
+ tldr_refresh_cache()
32
+ try:
33
+ all_docs.load()
34
+ except Exception as e:
35
+ log.error("Could not index docs: %s", e)
36
+ raise e
37
+ else:
38
+ version()
39
+ cprint()
40
+ terminal_feature_check().print_term_info()
41
+ cprint()
42
+ print_api_key_setup(once=False)
43
+ cprint()
44
+ check_tools(brief=brief)
45
+ cprint()
46
+ if tldr_refresh_cache():
47
+ cprint("Updated tldr cache")
48
+ else:
49
+ cprint("tldr cache is up to date")
50
+ try:
51
+ all_docs.load()
52
+ except Exception as e:
53
+ log.error("Could not index docs: %s", e)
54
+ raise e
55
+
56
+
57
+ @kash_command
58
+ def check_tools(warn_only: bool = False, brief: bool = False) -> None:
59
+ """
60
+ Check that all tools are installed.
61
+
62
+ :param warn_only: Only warn if tools are missing.
63
+ :param brief: Print summary as a single line.
64
+ """
65
+ if warn_only:
66
+ sys_tool_check().warn_if_missing()
67
+ else:
68
+ if brief:
69
+ cprint(sys_tool_check().status())
70
+ else:
71
+ cprint("Checking for required tools:")
72
+ cprint()
73
+ cprint(sys_tool_check().formatted())
74
+ cprint()
75
+ sys_tool_check().warn_if_missing()
76
+
77
+
78
+ @kash_command
79
+ def check_api_keys(warn_only: bool = False) -> None:
80
+ """
81
+ Check that all recommended API keys are set.
82
+ """
83
+
84
+ if warn_only:
85
+ warn_if_missing_api_keys()
86
+ else:
87
+ print_api_key_setup()
88
+
89
+
90
+ @kash_command
91
+ def kits() -> None:
92
+ """
93
+ List all kits (modules within `kash.kits`).
94
+ """
95
+ from kash.actions import get_loaded_kits
96
+
97
+ if not get_loaded_kits():
98
+ cprint(
99
+ "No kits currently imported (be sure the Python environment has `kash.kits` modules in the load path)"
100
+ )
101
+ else:
102
+ cprint("Currently imported kits:")
103
+ for kit in get_loaded_kits().values():
104
+ cprint(format_name_and_value(f"{kit.name} kit", str(kit.path or "")))
@@ -0,0 +1,41 @@
1
+ from kash.commands.base.basic_file_commands import trash
2
+ from kash.config.logger import get_logger
3
+ from kash.exec import kash_command
4
+ from kash.shell.output.shell_output import PrintHooks, cprint, format_name_and_value, print_h2
5
+
6
+ log = get_logger(__name__)
7
+
8
+
9
+ @kash_command
10
+ def global_settings() -> None:
11
+ """
12
+ Show all global kash settings.
13
+ """
14
+ from kash.config.settings import global_settings
15
+
16
+ settings = global_settings()
17
+ print_h2("Global Settings")
18
+ for field, value in settings.__dict__.items():
19
+ cprint(format_name_and_value(field, str(value)))
20
+ PrintHooks.spacer()
21
+
22
+
23
+ @kash_command
24
+ def clear_global_cache(media: bool = False, content: bool = False) -> None:
25
+ """
26
+ Clear the global media and content caches. By default clears both caches.
27
+
28
+ :param media: Clear the media cache only.
29
+ :param content: Clear the content cache only.
30
+ """
31
+ from kash.config.settings import global_settings
32
+
33
+ if not media and not content:
34
+ media = True
35
+ content = True
36
+
37
+ if media and global_settings().media_cache_dir.exists():
38
+ trash(global_settings().media_cache_dir)
39
+
40
+ if content and global_settings().content_cache_dir.exists():
41
+ trash(global_settings().content_cache_dir)