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.
- kash/__init__.py +2 -0
- kash/__main__.py +4 -0
- kash/actions/__init__.py +55 -0
- kash/actions/core/assistant_chat.py +45 -0
- kash/actions/core/chat.py +90 -0
- kash/actions/core/format_markdown_template.py +92 -0
- kash/actions/core/markdownify.py +29 -0
- kash/actions/core/readability.py +27 -0
- kash/actions/core/show_webpage.py +28 -0
- kash/actions/core/strip_html.py +28 -0
- kash/actions/core/summarize_as_bullets.py +53 -0
- kash/actions/core/webpage_config.py +21 -0
- kash/actions/core/webpage_generate.py +29 -0
- kash/actions/meta/write_instructions.py +39 -0
- kash/actions/meta/write_new_action.py +157 -0
- kash/commands/__init__.py +21 -0
- kash/commands/base/basic_file_commands.py +183 -0
- kash/commands/base/browser_commands.py +62 -0
- kash/commands/base/debug_commands.py +214 -0
- kash/commands/base/diff_commands.py +90 -0
- kash/commands/base/files_command.py +408 -0
- kash/commands/base/general_commands.py +104 -0
- kash/commands/base/global_state_commands.py +41 -0
- kash/commands/base/logs_commands.py +92 -0
- kash/commands/base/reformat_command.py +54 -0
- kash/commands/base/search_command.py +65 -0
- kash/commands/base/show_command.py +69 -0
- kash/commands/extras/utils_commands.py +27 -0
- kash/commands/help/assistant_commands.py +97 -0
- kash/commands/help/doc_commands.py +226 -0
- kash/commands/help/help_commands.py +133 -0
- kash/commands/workspace/selection_commands.py +200 -0
- kash/commands/workspace/workspace_commands.py +640 -0
- kash/concepts/concept_formats.py +23 -0
- kash/concepts/embeddings.py +130 -0
- kash/concepts/text_similarity.py +108 -0
- kash/config/__init__.py +4 -0
- kash/config/api_keys.py +84 -0
- kash/config/capture_output.py +77 -0
- kash/config/colors.py +279 -0
- kash/config/init.py +18 -0
- kash/config/lazy_imports.py +22 -0
- kash/config/logger.py +355 -0
- kash/config/logger_basic.py +35 -0
- kash/config/logo.txt +4 -0
- kash/config/logo_fancy.txt +4 -0
- kash/config/server_config.py +51 -0
- kash/config/settings.py +196 -0
- kash/config/setup.py +51 -0
- kash/config/suppress_warnings.py +27 -0
- kash/config/text_styles.py +426 -0
- kash/docs/__init__.py +0 -0
- kash/docs/all_docs.py +58 -0
- kash/docs/load_actions_info.py +28 -0
- kash/docs/load_api_docs.py +13 -0
- kash/docs/load_help_topics.py +47 -0
- kash/docs/load_source_code.py +125 -0
- kash/docs/markdown/api_docs_template.md +42 -0
- kash/docs/markdown/assistant_instructions_template.md +114 -0
- kash/docs/markdown/readme_template.md +26 -0
- kash/docs/markdown/topics/a1_what_is_kash.md +76 -0
- kash/docs/markdown/topics/a2_progress.md +96 -0
- kash/docs/markdown/topics/a3_installation.md +119 -0
- kash/docs/markdown/topics/a4_getting_started.md +300 -0
- kash/docs/markdown/topics/a5_tips_for_use_with_other_tools.md +83 -0
- kash/docs/markdown/topics/b0_philosophy_of_kash.md +177 -0
- kash/docs/markdown/topics/b1_kash_overview.md +124 -0
- kash/docs/markdown/topics/b2_workspace_and_file_formats.md +61 -0
- kash/docs/markdown/topics/b3_modern_shell_tool_recommendations.md +83 -0
- kash/docs/markdown/topics/b4_faq.md +166 -0
- kash/docs/markdown/warning.md +7 -0
- kash/docs/markdown/welcome.md +9 -0
- kash/docs_base/docs_base.py +85 -0
- kash/docs_base/load_custom_command_info.py +27 -0
- kash/docs_base/load_faqs.py +48 -0
- kash/docs_base/load_recipe_snippets.py +48 -0
- kash/docs_base/recipes/general_system_commands.ksh +10 -0
- kash/docs_base/recipes/python_dev_commands.ksh +7 -0
- kash/docs_base/recipes/tldr_standard_commands.ksh +2144 -0
- kash/errors.py +176 -0
- kash/exec/__init__.py +16 -0
- kash/exec/action_decorators.py +412 -0
- kash/exec/action_exec.py +457 -0
- kash/exec/action_registry.py +123 -0
- kash/exec/combiners.py +127 -0
- kash/exec/command_exec.py +34 -0
- kash/exec/command_registry.py +72 -0
- kash/exec/fetch_url_metadata.py +71 -0
- kash/exec/history.py +44 -0
- kash/exec/llm_transforms.py +121 -0
- kash/exec/precondition_checks.py +71 -0
- kash/exec/precondition_registry.py +43 -0
- kash/exec/preconditions.py +152 -0
- kash/exec/resolve_args.py +123 -0
- kash/exec/shell_callable_action.py +90 -0
- kash/exec_model/__init__.py +0 -0
- kash/exec_model/args_model.py +93 -0
- kash/exec_model/commands_model.py +163 -0
- kash/exec_model/script_model.py +161 -0
- kash/exec_model/shell_model.py +21 -0
- kash/file_storage/__init__.py +0 -0
- kash/file_storage/file_store.py +642 -0
- kash/file_storage/item_file_format.py +152 -0
- kash/file_storage/metadata_dirs.py +108 -0
- kash/file_storage/mtime_cache.py +108 -0
- kash/file_storage/persisted_yaml.py +37 -0
- kash/file_storage/store_cache_warmer.py +37 -0
- kash/file_storage/store_filenames.py +53 -0
- kash/form_input/__init__.py +0 -0
- kash/form_input/prompt_input.py +44 -0
- kash/help/__init__.py +0 -0
- kash/help/assistant.py +324 -0
- kash/help/assistant_instructions.py +68 -0
- kash/help/assistant_output.py +43 -0
- kash/help/docstring_utils.py +111 -0
- kash/help/function_param_info.py +44 -0
- kash/help/help_embeddings.py +85 -0
- kash/help/help_lookups.py +60 -0
- kash/help/help_pages.py +122 -0
- kash/help/help_printing.py +169 -0
- kash/help/help_types.py +247 -0
- kash/help/recommended_commands.py +143 -0
- kash/help/tldr_help.py +296 -0
- kash/llm_utils/__init__.py +0 -0
- kash/llm_utils/chat_format.py +413 -0
- kash/llm_utils/clean_headings.py +65 -0
- kash/llm_utils/fuzzy_parsing.py +119 -0
- kash/llm_utils/language_models.py +178 -0
- kash/llm_utils/llm_completion.py +172 -0
- kash/llm_utils/llm_messages.py +36 -0
- kash/local_server/__init__.py +2 -0
- kash/local_server/local_server.py +183 -0
- kash/local_server/local_server_commands.py +55 -0
- kash/local_server/local_server_routes.py +306 -0
- kash/local_server/local_url_formatters.py +169 -0
- kash/local_server/port_tools.py +67 -0
- kash/local_server/rich_html_template.py +12 -0
- kash/mcp/__init__.py +2 -0
- kash/mcp/mcp_main.py +67 -0
- kash/mcp/mcp_server_commands.py +57 -0
- kash/mcp/mcp_server_routes.py +256 -0
- kash/mcp/mcp_server_sse.py +143 -0
- kash/mcp/mcp_server_stdio.py +45 -0
- kash/media_base/__init__.py +0 -0
- kash/media_base/audio_processing.py +27 -0
- kash/media_base/media_cache.py +178 -0
- kash/media_base/media_services.py +112 -0
- kash/media_base/media_tools.py +48 -0
- kash/media_base/services/local_file_media.py +165 -0
- kash/media_base/speech_transcription.py +224 -0
- kash/media_base/timestamp_citations.py +80 -0
- kash/model/__init__.py +73 -0
- kash/model/actions_model.py +633 -0
- kash/model/assistant_response_model.py +87 -0
- kash/model/compound_actions_model.py +188 -0
- kash/model/graph_model.py +92 -0
- kash/model/items_model.py +821 -0
- kash/model/language_list.py +39 -0
- kash/model/llm_actions_model.py +63 -0
- kash/model/media_model.py +124 -0
- kash/model/operations_model.py +176 -0
- kash/model/params_model.py +435 -0
- kash/model/paths_model.py +458 -0
- kash/model/preconditions_model.py +98 -0
- kash/shell/__init__.py +0 -0
- kash/shell/completions/completion_scoring.py +280 -0
- kash/shell/completions/completion_types.py +154 -0
- kash/shell/completions/shell_completions.py +277 -0
- kash/shell/file_icons/color_for_format.py +70 -0
- kash/shell/file_icons/nerd_icons.py +946 -0
- kash/shell/output/__init__.py +0 -0
- kash/shell/output/kerm_code_utils.py +59 -0
- kash/shell/output/kerm_codes.py +588 -0
- kash/shell/output/kmarkdown.py +117 -0
- kash/shell/output/shell_output.py +477 -0
- kash/shell/ui/__init__.py +0 -0
- kash/shell/ui/shell_results.py +118 -0
- kash/shell/ui/shell_syntax.py +26 -0
- kash/shell/utils/exception_printing.py +50 -0
- kash/shell/utils/native_utils.py +240 -0
- kash/shell/utils/osc_utils.py +95 -0
- kash/shell/utils/shell_function_wrapper.py +204 -0
- kash/shell/utils/sys_tool_deps.py +289 -0
- kash/shell/utils/terminal_images.py +133 -0
- kash/shell_main.py +67 -0
- kash/text_handling/custom_sliding_transforms.py +266 -0
- kash/text_handling/doc_normalization.py +64 -0
- kash/text_handling/markdown_util.py +167 -0
- kash/text_handling/unified_diffs.py +138 -0
- kash/utils/__init__.py +4 -0
- kash/utils/common/__init__.py +4 -0
- kash/utils/common/atomic_var.py +147 -0
- kash/utils/common/format_utils.py +81 -0
- kash/utils/common/function_inspect.py +178 -0
- kash/utils/common/import_utils.py +89 -0
- kash/utils/common/lazyobject.py +144 -0
- kash/utils/common/obj_replace.py +78 -0
- kash/utils/common/parse_key_vals.py +85 -0
- kash/utils/common/parse_shell_args.py +348 -0
- kash/utils/common/stack_traces.py +49 -0
- kash/utils/common/string_replace.py +93 -0
- kash/utils/common/string_template.py +101 -0
- kash/utils/common/task_stack.py +162 -0
- kash/utils/common/type_utils.py +137 -0
- kash/utils/common/uniquifier.py +95 -0
- kash/utils/common/url.py +155 -0
- kash/utils/file_utils/__init__.py +3 -0
- kash/utils/file_utils/dir_size.py +48 -0
- kash/utils/file_utils/file_ext.py +86 -0
- kash/utils/file_utils/file_formats.py +134 -0
- kash/utils/file_utils/file_formats_model.py +408 -0
- kash/utils/file_utils/file_sort_filter.py +235 -0
- kash/utils/file_utils/file_walk.py +163 -0
- kash/utils/file_utils/filename_parsing.py +99 -0
- kash/utils/file_utils/git_tools.py +19 -0
- kash/utils/file_utils/ignore_files.py +166 -0
- kash/utils/file_utils/path_utils.py +36 -0
- kash/utils/lang_utils/__init__.py +0 -0
- kash/utils/lang_utils/capitalization.py +128 -0
- kash/utils/lang_utils/inflection.py +18 -0
- kash/utils/rich_custom/__init__.py +3 -0
- kash/utils/rich_custom/ansi_cell_len.py +72 -0
- kash/utils/rich_custom/rich_char_transform.py +89 -0
- kash/utils/rich_custom/rich_indent.py +69 -0
- kash/utils/rich_custom/rich_markdown_fork.py +771 -0
- kash/version.py +31 -0
- kash/web_content/canon_url.py +24 -0
- kash/web_content/dir_store.py +103 -0
- kash/web_content/file_cache_utils.py +117 -0
- kash/web_content/local_file_cache.py +247 -0
- kash/web_content/web_extract.py +55 -0
- kash/web_content/web_extract_justext.py +86 -0
- kash/web_content/web_extract_readabilipy.py +23 -0
- kash/web_content/web_fetch.py +101 -0
- kash/web_content/web_page_model.py +28 -0
- kash/web_gen/__init__.py +4 -0
- kash/web_gen/tabbed_webpage.py +149 -0
- kash/web_gen/template_render.py +29 -0
- kash/web_gen/templates/base_styles.css.jinja +192 -0
- kash/web_gen/templates/base_webpage.html.jinja +124 -0
- kash/web_gen/templates/content_styles.css.jinja +194 -0
- kash/web_gen/templates/explain_view.html.jinja +49 -0
- kash/web_gen/templates/item_view.html.jinja +294 -0
- kash/web_gen/templates/tabbed_webpage.html.jinja +49 -0
- kash/workspaces/__init__.py +13 -0
- kash/workspaces/param_state.py +24 -0
- kash/workspaces/selections.py +333 -0
- kash/workspaces/source_items.py +88 -0
- kash/workspaces/workspace_importing.py +56 -0
- kash/workspaces/workspace_names.py +33 -0
- kash/workspaces/workspace_output.py +154 -0
- kash/workspaces/workspace_registry.py +78 -0
- kash/workspaces/workspaces.py +197 -0
- kash/xonsh_custom/custom_shell.py +366 -0
- kash/xonsh_custom/customize_prompt.py +197 -0
- kash/xonsh_custom/customize_xonsh.py +112 -0
- kash/xonsh_custom/shell_load_commands.py +152 -0
- kash/xonsh_custom/shell_which.py +64 -0
- kash/xonsh_custom/xonsh_completers.py +715 -0
- kash/xonsh_custom/xonsh_env.py +28 -0
- kash/xonsh_custom/xonsh_modern_tools.py +56 -0
- kash/xonsh_custom/xonsh_ranking_completer.py +152 -0
- kash/xontrib/fnm.py +120 -0
- kash/xontrib/kash_extension.py +61 -0
- kash_shell-0.3.0.dist-info/METADATA +757 -0
- kash_shell-0.3.0.dist-info/RECORD +269 -0
- kash_shell-0.3.0.dist-info/WHEEL +4 -0
- kash_shell-0.3.0.dist-info/entry_points.txt +3 -0
- kash_shell-0.3.0.dist-info/licenses/LICENSE +664 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import re
|
|
2
|
+
|
|
3
|
+
from kash.utils.common.parse_shell_args import shell_quote
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_assist_request_str(line: str) -> str | None:
|
|
7
|
+
"""
|
|
8
|
+
Is this a query to the assistant?
|
|
9
|
+
Checks for phrases ending in a ? or starting with a ?.
|
|
10
|
+
"""
|
|
11
|
+
line = line.strip()
|
|
12
|
+
if re.search(r"\b\w+\?$", line) or line.startswith("?"):
|
|
13
|
+
return line.lstrip("?").strip()
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def assist_request_str(nl_req: str) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Command string to call the assistant with a natural language request.
|
|
20
|
+
"""
|
|
21
|
+
nl_req = nl_req.lstrip("? ").rstrip()
|
|
22
|
+
# Quoting isn't necessary unless we have quote marks.
|
|
23
|
+
if "'" in nl_req or '"' in nl_req:
|
|
24
|
+
return f"? {shell_quote(nl_req, idempotent=True)}"
|
|
25
|
+
else:
|
|
26
|
+
return f"? {nl_req}"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import TypeVar
|
|
4
|
+
|
|
5
|
+
from kash.config.logger import get_logger
|
|
6
|
+
from kash.config.text_styles import COLOR_ERROR
|
|
7
|
+
from kash.errors import NONFATAL_EXCEPTIONS
|
|
8
|
+
from kash.shell.output.shell_output import PrintHooks
|
|
9
|
+
|
|
10
|
+
log = get_logger(__name__)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def summarize_traceback(exception: Exception) -> str:
|
|
14
|
+
exception_str = str(exception)
|
|
15
|
+
lines = exception_str.splitlines()
|
|
16
|
+
exc_type = type(exception).__name__
|
|
17
|
+
return f"{exc_type}: " + "\n".join(
|
|
18
|
+
[
|
|
19
|
+
line
|
|
20
|
+
for line in lines
|
|
21
|
+
if line.strip()
|
|
22
|
+
and not line.lstrip().startswith("Traceback")
|
|
23
|
+
# and not line.lstrip().startswith("File ")
|
|
24
|
+
and not line.lstrip().startswith("The above exception")
|
|
25
|
+
and not line.startswith(" ")
|
|
26
|
+
]
|
|
27
|
+
+ ["\nRun `logs` for details."]
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
R = TypeVar("R")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def wrap_with_exception_printing(func: Callable[..., R]) -> Callable[[list[str]], R | None]:
|
|
35
|
+
@wraps(func)
|
|
36
|
+
def command(*args) -> R | None:
|
|
37
|
+
try:
|
|
38
|
+
log.info(
|
|
39
|
+
"Command function call: %s(%s)",
|
|
40
|
+
func.__name__,
|
|
41
|
+
(", ".join(str(arg) for arg in args)),
|
|
42
|
+
)
|
|
43
|
+
return func(*args)
|
|
44
|
+
except NONFATAL_EXCEPTIONS as e:
|
|
45
|
+
PrintHooks.nonfatal_exception()
|
|
46
|
+
log.error(f"[{COLOR_ERROR}]Command error:[/{COLOR_ERROR}] %s", summarize_traceback(e))
|
|
47
|
+
log.info("Command error details: %s", e, exc_info=True)
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
return command
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Platform-specific tools and utilities.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shlex
|
|
7
|
+
import subprocess
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import webbrowser
|
|
10
|
+
from enum import Enum
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from flowmark import Wrap
|
|
14
|
+
from funlog import log_calls
|
|
15
|
+
|
|
16
|
+
from kash.config.logger import get_logger
|
|
17
|
+
from kash.config.text_styles import BAT_STYLE, BAT_THEME, COLOR_ERROR
|
|
18
|
+
from kash.errors import FileNotFound, SetupError
|
|
19
|
+
from kash.shell.output.shell_output import cprint
|
|
20
|
+
from kash.shell.utils.sys_tool_deps import PLATFORM, Platform, SysTool, sys_tool_check
|
|
21
|
+
from kash.shell.utils.terminal_images import terminal_show_image
|
|
22
|
+
from kash.utils.common.format_utils import fmt_loc
|
|
23
|
+
from kash.utils.common.url import as_file_url, is_file_url, is_url
|
|
24
|
+
from kash.utils.file_utils.file_formats import is_full_html_page, read_partial_text
|
|
25
|
+
from kash.utils.file_utils.file_formats_model import file_format_info
|
|
26
|
+
|
|
27
|
+
log = get_logger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def file_size_check(
|
|
31
|
+
filename: str | Path, max_lines: int = 100, max_bytes: int = 50 * 1024
|
|
32
|
+
) -> tuple[int, int]:
|
|
33
|
+
"""
|
|
34
|
+
Get the size and scan to get initial line count (up to max_lines) of a file.
|
|
35
|
+
"""
|
|
36
|
+
filename = str(filename)
|
|
37
|
+
file_size = os.path.getsize(filename)
|
|
38
|
+
line_min = 0
|
|
39
|
+
with open(filename, "rb") as f:
|
|
40
|
+
for i, _line in enumerate(f):
|
|
41
|
+
if i >= max_lines or f.tell() > max_bytes:
|
|
42
|
+
break
|
|
43
|
+
line_min += 1
|
|
44
|
+
return file_size, line_min
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def native_open(filename: str | Path):
|
|
48
|
+
filename = str(filename)
|
|
49
|
+
log.message("Opening file: %s", filename)
|
|
50
|
+
if PLATFORM == Platform.Darwin:
|
|
51
|
+
subprocess.run(["open", filename])
|
|
52
|
+
elif PLATFORM == Platform.Linux:
|
|
53
|
+
subprocess.run(["xdg-open", filename])
|
|
54
|
+
elif PLATFORM == Platform.Windows:
|
|
55
|
+
subprocess.run(["start", shlex.quote(filename)], shell=True)
|
|
56
|
+
else:
|
|
57
|
+
raise NotImplementedError("Unsupported platform")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def native_open_url(url_or_query: str):
|
|
61
|
+
"""
|
|
62
|
+
Open a URL or query in the system's default browser.
|
|
63
|
+
"""
|
|
64
|
+
log.message("Opening in browser: %s", url_or_query)
|
|
65
|
+
if is_url(url_or_query):
|
|
66
|
+
webbrowser.open(url_or_query)
|
|
67
|
+
else:
|
|
68
|
+
webbrowser.open(f"https://www.google.com/search?q={urllib.parse.quote(url_or_query)}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ViewMode(Enum):
|
|
72
|
+
auto = "auto"
|
|
73
|
+
console = "console"
|
|
74
|
+
browser = "browser"
|
|
75
|
+
native = "native"
|
|
76
|
+
terminal_image = "terminal_image"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@log_calls(level="info")
|
|
80
|
+
def _detect_view_mode(file_or_url: str) -> ViewMode:
|
|
81
|
+
# As a heuristic, we use the browser for URLs and for local files that are
|
|
82
|
+
# clearly full HTML pages (since HTML fragments are fine on console).
|
|
83
|
+
if is_url(file_or_url) and not is_file_url(file_or_url):
|
|
84
|
+
return ViewMode.browser
|
|
85
|
+
|
|
86
|
+
path = Path(file_or_url)
|
|
87
|
+
if path.is_file(): # File or symlink.
|
|
88
|
+
content = read_partial_text(path)
|
|
89
|
+
if content and is_full_html_page(content):
|
|
90
|
+
return ViewMode.browser
|
|
91
|
+
|
|
92
|
+
info = file_format_info(path)
|
|
93
|
+
log.info("File format detected: %s", info)
|
|
94
|
+
|
|
95
|
+
if info.is_text:
|
|
96
|
+
return ViewMode.console
|
|
97
|
+
if info.is_image:
|
|
98
|
+
log.info("Detected image file, will display in terminal")
|
|
99
|
+
return ViewMode.terminal_image
|
|
100
|
+
else:
|
|
101
|
+
return ViewMode.native
|
|
102
|
+
elif path.is_dir():
|
|
103
|
+
return ViewMode.native
|
|
104
|
+
else:
|
|
105
|
+
raise FileNotFound(fmt_loc(file_or_url))
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def view_file_native(
|
|
109
|
+
file_or_url: str | Path,
|
|
110
|
+
view_mode: ViewMode = ViewMode.auto,
|
|
111
|
+
):
|
|
112
|
+
"""
|
|
113
|
+
Open a file or URL in the console or a native app. If `view_mode` is auto,
|
|
114
|
+
automatically determine whether to use console, web browser, or the user's
|
|
115
|
+
preferred native application. For images, also tries terminal-based image
|
|
116
|
+
display.
|
|
117
|
+
"""
|
|
118
|
+
file_or_url = str(file_or_url)
|
|
119
|
+
path = None
|
|
120
|
+
if not is_url(file_or_url):
|
|
121
|
+
path = Path(file_or_url)
|
|
122
|
+
if not path.exists():
|
|
123
|
+
raise FileNotFound(fmt_loc(path))
|
|
124
|
+
|
|
125
|
+
if view_mode == ViewMode.auto:
|
|
126
|
+
view_mode = _detect_view_mode(file_or_url)
|
|
127
|
+
|
|
128
|
+
if view_mode == ViewMode.browser:
|
|
129
|
+
url = file_or_url if is_url(file_or_url) else as_file_url(file_or_url)
|
|
130
|
+
log.message("Opening URL in browser: %s", url)
|
|
131
|
+
webbrowser.open(url)
|
|
132
|
+
elif view_mode == ViewMode.console and path:
|
|
133
|
+
file_size, min_lines = file_size_check(path)
|
|
134
|
+
view_file_console(path, use_pager=min_lines > 40 or file_size > 20 * 1024)
|
|
135
|
+
elif view_mode == ViewMode.terminal_image and path:
|
|
136
|
+
try:
|
|
137
|
+
terminal_show_image(path)
|
|
138
|
+
except SetupError as e:
|
|
139
|
+
log.info("%s: %s", e, path)
|
|
140
|
+
native_open(path)
|
|
141
|
+
elif view_mode == ViewMode.native:
|
|
142
|
+
native_open(file_or_url)
|
|
143
|
+
else:
|
|
144
|
+
raise ValueError(f"Don't know how to view: {view_mode}: {file_or_url}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def tail_file(
|
|
148
|
+
filename: str | Path,
|
|
149
|
+
follow: bool = False,
|
|
150
|
+
max_lines: int = 10000,
|
|
151
|
+
follow_max_lines: int = 200,
|
|
152
|
+
):
|
|
153
|
+
"""
|
|
154
|
+
Tail a log file. With colorization using bat if available, otherwise using less.
|
|
155
|
+
If follow is True, follows the file as it grows.
|
|
156
|
+
|
|
157
|
+
Uses bat if available. Note bat doesn't have efficient seek functionality like
|
|
158
|
+
`less +G` so we prefer to use bat with less. Use Ctrl-C to quit less (this is
|
|
159
|
+
enabled with `less -K`).
|
|
160
|
+
"""
|
|
161
|
+
filename = str(filename)
|
|
162
|
+
quoted_filename = shlex.quote(filename)
|
|
163
|
+
|
|
164
|
+
if follow:
|
|
165
|
+
max_lines = follow_max_lines
|
|
166
|
+
|
|
167
|
+
sys_tool_check().require(SysTool.less)
|
|
168
|
+
sys_tool_check().warn_if_missing(SysTool.bat, SysTool.tail)
|
|
169
|
+
|
|
170
|
+
if follow:
|
|
171
|
+
if sys_tool_check().has(SysTool.bat, SysTool.tail, SysTool.less):
|
|
172
|
+
# Follow the file in real-time.
|
|
173
|
+
command = (
|
|
174
|
+
f"tail -{max_lines} -f {quoted_filename} | "
|
|
175
|
+
f"bat --paging=never --color=always --style=plain --theme={BAT_THEME} -l log | "
|
|
176
|
+
"less -K -R +F"
|
|
177
|
+
)
|
|
178
|
+
else:
|
|
179
|
+
command = f"tail -f {quoted_filename} | less -R +F"
|
|
180
|
+
cprint("Following file: `%s`", command, text_wrap=Wrap.NONE)
|
|
181
|
+
else:
|
|
182
|
+
if sys_tool_check().has(SysTool.bat, SysTool.tail, SysTool.less):
|
|
183
|
+
command = (
|
|
184
|
+
f"tail -{max_lines} {quoted_filename} | "
|
|
185
|
+
f"bat --paging=never --color=always --style=plain --theme={BAT_THEME} -l log | "
|
|
186
|
+
"less -K -R +G"
|
|
187
|
+
)
|
|
188
|
+
else:
|
|
189
|
+
command = f"less +G {quoted_filename}"
|
|
190
|
+
cprint("Tailing file: `%s`", command, text_wrap=Wrap.NONE)
|
|
191
|
+
|
|
192
|
+
subprocess.run(command, shell=True, check=True)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def view_file_console(filename: str | Path, use_pager: bool = True):
|
|
196
|
+
"""
|
|
197
|
+
Displays a file in the console with pagination and syntax highlighting.
|
|
198
|
+
"""
|
|
199
|
+
filename = str(filename)
|
|
200
|
+
quoted_filename = shlex.quote(filename)
|
|
201
|
+
|
|
202
|
+
# TODO: Visualize YAML frontmatter with different syntax/style than Markdown content.
|
|
203
|
+
|
|
204
|
+
is_text = file_format_info(filename).is_text
|
|
205
|
+
if is_text:
|
|
206
|
+
sys_tool_check().require(SysTool.less)
|
|
207
|
+
if sys_tool_check().has(SysTool.bat):
|
|
208
|
+
pager_str = "--pager=always --pager=less " if use_pager else ""
|
|
209
|
+
command = f"bat {pager_str}--color=always --style={BAT_STYLE} --theme={BAT_THEME} {quoted_filename}"
|
|
210
|
+
else:
|
|
211
|
+
sys_tool_check().require(SysTool.pygmentize)
|
|
212
|
+
command = f"pygmentize -g {quoted_filename}"
|
|
213
|
+
if use_pager:
|
|
214
|
+
command = f"{command} | less -R"
|
|
215
|
+
else:
|
|
216
|
+
sys_tool_check().require(SysTool.hexyl)
|
|
217
|
+
command = f"hexyl {quoted_filename}"
|
|
218
|
+
if use_pager:
|
|
219
|
+
command = f"{command} | less -R"
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
subprocess.run(command, shell=True, check=True)
|
|
223
|
+
except subprocess.CalledProcessError as e:
|
|
224
|
+
cprint(f"Error displaying file: {e}", style=COLOR_ERROR, text_wrap=Wrap.NONE)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def edit_files(*filenames: str | Path):
|
|
228
|
+
"""
|
|
229
|
+
Edit a file using the user's preferred editor.
|
|
230
|
+
"""
|
|
231
|
+
from kash.config.settings import global_settings
|
|
232
|
+
|
|
233
|
+
editor = os.getenv("EDITOR", global_settings().default_editor)
|
|
234
|
+
subprocess.run([editor] + list(filenames))
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def native_trash(*paths: str | Path):
|
|
238
|
+
from send2trash import send2trash
|
|
239
|
+
|
|
240
|
+
send2trash(list(Path(p) for p in paths))
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from functools import cache
|
|
3
|
+
|
|
4
|
+
from rich.style import Style
|
|
5
|
+
from rich.text import Text
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@cache
|
|
9
|
+
def terminal_supports_osc8() -> bool:
|
|
10
|
+
"""
|
|
11
|
+
Attempt to detect if the terminal supports OSC 8 hyperlinks.
|
|
12
|
+
"""
|
|
13
|
+
term_program = os.environ.get("TERM_PROGRAM", "")
|
|
14
|
+
term = os.environ.get("TERM", "")
|
|
15
|
+
|
|
16
|
+
if term_program in ["iTerm.app", "WezTerm", "Hyper"]:
|
|
17
|
+
return True
|
|
18
|
+
if "konsole" in term_program.lower():
|
|
19
|
+
return True
|
|
20
|
+
if "kitty" in term or "xterm" in term:
|
|
21
|
+
return True
|
|
22
|
+
if "vscode" in term_program.lower():
|
|
23
|
+
return True
|
|
24
|
+
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# Constants for OSC control sequences
|
|
29
|
+
OSC_START = "\x1b]"
|
|
30
|
+
ST_CODE = "\x1b\\" # String Terminator
|
|
31
|
+
BEL_CODE = "\x07" # Bell character
|
|
32
|
+
|
|
33
|
+
OSC_HYPERLINK = "8"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class OscStr(str):
|
|
37
|
+
"""
|
|
38
|
+
Marker class for strings that contain OSC codes.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def osc_code(code: int | str, data: str) -> OscStr:
|
|
45
|
+
"""
|
|
46
|
+
Return an extended OSC code.
|
|
47
|
+
"""
|
|
48
|
+
return OscStr(f"{OSC_START}{code};{data}{ST_CODE}")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def osc8_link_codes(uri: str, metadata_str: str = "") -> tuple[str, str]:
|
|
52
|
+
safe_uri = uri.replace(";", "%3B") # Escape semicolons in the URL.
|
|
53
|
+
safe_metadata = metadata_str.replace(";", "%3B")
|
|
54
|
+
escape_start = f"{OSC_START}{OSC_HYPERLINK};{safe_metadata};{safe_uri}{ST_CODE}"
|
|
55
|
+
escape_end = f"{OSC_START}{OSC_HYPERLINK};;{ST_CODE}"
|
|
56
|
+
return escape_start, escape_end
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def osc8_link(uri: str, text: str, metadata_str: str = "") -> OscStr:
|
|
60
|
+
r"""
|
|
61
|
+
Return a string with the OSC 8 hyperlink escape sequence.
|
|
62
|
+
|
|
63
|
+
Format: ESC ] 8 ; params ; URI ST text ESC ] 8 ; ; ST
|
|
64
|
+
|
|
65
|
+
ST (String Terminator) is either ESC \ or BEL but the former is more common.
|
|
66
|
+
|
|
67
|
+
Spec: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
|
|
68
|
+
|
|
69
|
+
:param uri: The URI or URL for the hyperlink.
|
|
70
|
+
:param text: The clickable text to display.
|
|
71
|
+
:param metadata_str: Optional metadata between the semicolons.
|
|
72
|
+
"""
|
|
73
|
+
escape_start, escape_end = osc8_link_codes(uri, metadata_str)
|
|
74
|
+
|
|
75
|
+
return OscStr(f"{escape_start}{text}{escape_end}")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def osc8_link_graceful(uri: str, text: str, id: str = "") -> OscStr | str:
|
|
79
|
+
"""
|
|
80
|
+
Generate clickable text for terminal emulators supporting OSC 8 with a fallback
|
|
81
|
+
for non-supporting terminals to make the link visible.
|
|
82
|
+
"""
|
|
83
|
+
if terminal_supports_osc8():
|
|
84
|
+
metadata_str = f"id={id}" if id else ""
|
|
85
|
+
return osc8_link(uri, text, metadata_str)
|
|
86
|
+
else:
|
|
87
|
+
# Fallback for non-supporting terminals.
|
|
88
|
+
return f"{text} ({uri})"
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def osc8_link_rich(uri: str, text: str, metadata_str: str = "", style: str | Style = "") -> Text:
|
|
92
|
+
"""
|
|
93
|
+
Must use Text.from_ansi() for Rich to handle links correctly!
|
|
94
|
+
"""
|
|
95
|
+
return Text.from_ansi(osc8_link(uri, text, metadata_str), style=style)
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import types
|
|
2
|
+
from collections.abc import Callable, Mapping
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from functools import wraps
|
|
5
|
+
from typing import Any, TypeVar, cast, get_args
|
|
6
|
+
|
|
7
|
+
from kash.config.logger import get_logger
|
|
8
|
+
from kash.errors import InvalidCommand
|
|
9
|
+
from kash.exec.command_registry import CommandFunction
|
|
10
|
+
from kash.help.help_printing import print_command_function_help
|
|
11
|
+
from kash.utils.common.function_inspect import FuncParam, inspect_function_params
|
|
12
|
+
from kash.utils.common.parse_shell_args import parse_shell_args
|
|
13
|
+
|
|
14
|
+
log = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _map_positional(
|
|
18
|
+
pos_args: list[str], pos_params: list[FuncParam], kw_params: list[FuncParam]
|
|
19
|
+
) -> tuple[list[Any], int]:
|
|
20
|
+
"""
|
|
21
|
+
Map parsed positional arguments to function parameters, ensuring the number of
|
|
22
|
+
arguments matches and converting types.
|
|
23
|
+
"""
|
|
24
|
+
pos_values = []
|
|
25
|
+
i = 0
|
|
26
|
+
keywords_consumed = 0
|
|
27
|
+
|
|
28
|
+
for param in pos_params:
|
|
29
|
+
param_type = param.type or str
|
|
30
|
+
if param.is_varargs:
|
|
31
|
+
pos_values.extend([param_type(arg) for arg in pos_args[i:]])
|
|
32
|
+
return pos_values, 0 # All remaining args are consumed, so we can return early.
|
|
33
|
+
elif i < len(pos_args):
|
|
34
|
+
pos_values.append(param_type(pos_args[i]))
|
|
35
|
+
i += 1
|
|
36
|
+
else:
|
|
37
|
+
raise InvalidCommand(f"Missing positional argument: {param.name}")
|
|
38
|
+
|
|
39
|
+
# If there are remaining positional arguments, they will go toward keyword arguments.
|
|
40
|
+
for param in kw_params:
|
|
41
|
+
param_type = param.type or str
|
|
42
|
+
if not param.is_varargs and i < len(pos_args):
|
|
43
|
+
pos_values.append(param_type(pos_args[i]))
|
|
44
|
+
i += 1
|
|
45
|
+
keywords_consumed += 1
|
|
46
|
+
|
|
47
|
+
if i < len(pos_args):
|
|
48
|
+
raise InvalidCommand(
|
|
49
|
+
f"Too many arguments provided (expected {len(pos_params)}, got {len(pos_args)}): {pos_args}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return pos_values, keywords_consumed
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _map_keyword(kw_args: Mapping[str, str | bool], kw_params: list[FuncParam]) -> dict[str, Any]:
|
|
56
|
+
"""
|
|
57
|
+
Map parsed keyword arguments to function parameters, converting types and handling var
|
|
58
|
+
keyword arguments.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
kw_values = {param.name: param.default for param in kw_params if not param.is_varargs}
|
|
62
|
+
var_kw_values = {}
|
|
63
|
+
var_kw_param = None
|
|
64
|
+
|
|
65
|
+
# Find the var keyword argument (**kwargs), if any.
|
|
66
|
+
var_kw_param = next((param for param in kw_params if param.is_varargs), None)
|
|
67
|
+
|
|
68
|
+
# Map the keyword arguments to the function parameters.
|
|
69
|
+
for key, value in kw_args.items():
|
|
70
|
+
matching_param = next((param for param in kw_params if param.name == key), None)
|
|
71
|
+
if matching_param:
|
|
72
|
+
matching_param_type = matching_param.type or str
|
|
73
|
+
|
|
74
|
+
# Handle UnionType (str | None) specially
|
|
75
|
+
if hasattr(types, "UnionType") and isinstance(matching_param_type, types.UnionType):
|
|
76
|
+
args = get_args(matching_param_type)
|
|
77
|
+
non_none_args = [arg for arg in args if arg is not type(None)]
|
|
78
|
+
if len(non_none_args) == 1 and isinstance(non_none_args[0], type):
|
|
79
|
+
matching_param_type = non_none_args[0]
|
|
80
|
+
|
|
81
|
+
if isinstance(value, bool) and not issubclass(matching_param_type, bool):
|
|
82
|
+
raise InvalidCommand(f"Option `--{key}` expects a value")
|
|
83
|
+
if not isinstance(value, bool) and issubclass(matching_param_type, bool):
|
|
84
|
+
raise InvalidCommand(f"Option `--{key}` is boolean and does not take a value")
|
|
85
|
+
|
|
86
|
+
try:
|
|
87
|
+
kw_values[key] = matching_param_type(value) # Convert value to type.
|
|
88
|
+
except Exception as e:
|
|
89
|
+
valid_values = ""
|
|
90
|
+
if isinstance(matching_param.type, type) and issubclass(matching_param.type, Enum):
|
|
91
|
+
valid_values = f" (valid values are: {', '.join('`' + v.name + '`' for v in matching_param.type)})"
|
|
92
|
+
raise InvalidCommand(
|
|
93
|
+
f"Invalid value for option `{key}`: {value}{valid_values}"
|
|
94
|
+
) from e
|
|
95
|
+
elif var_kw_param:
|
|
96
|
+
var_kw_values[key] = value
|
|
97
|
+
else:
|
|
98
|
+
raise InvalidCommand(f"Unknown option `--{key}`")
|
|
99
|
+
|
|
100
|
+
if var_kw_param:
|
|
101
|
+
kw_values.update(var_kw_values)
|
|
102
|
+
|
|
103
|
+
return kw_values
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
R = TypeVar("R")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def wrap_for_shell_args(func: Callable[..., R]) -> Callable[[list[str]], R | None]:
|
|
110
|
+
"""
|
|
111
|
+
Wrap a function to accept a list of string shell-style arguments, parse them, and
|
|
112
|
+
call the original function.
|
|
113
|
+
"""
|
|
114
|
+
from kash.commands.help import help_commands
|
|
115
|
+
|
|
116
|
+
params = inspect_function_params(func)
|
|
117
|
+
pos_params = [p for p in params if p.is_positional]
|
|
118
|
+
kw_params = [p for p in params if p not in pos_params]
|
|
119
|
+
|
|
120
|
+
@wraps(func)
|
|
121
|
+
def wrapped(args: list[str]) -> R | None:
|
|
122
|
+
shell_args = parse_shell_args(args)
|
|
123
|
+
|
|
124
|
+
if shell_args.show_help:
|
|
125
|
+
print_command_function_help(cast(CommandFunction, func), verbose=True)
|
|
126
|
+
return None
|
|
127
|
+
elif shell_args.options.get("show_source", False):
|
|
128
|
+
help_commands.source_code(func.__name__)
|
|
129
|
+
return None
|
|
130
|
+
|
|
131
|
+
pos_values, kw_consumed = _map_positional(shell_args.args, pos_params, kw_params)
|
|
132
|
+
|
|
133
|
+
# If some positional arguments were used as keyword arguments, we need to remove
|
|
134
|
+
# them from the kw_params so they don't get passed twice.
|
|
135
|
+
remaining_kw_params = kw_params[kw_consumed:]
|
|
136
|
+
|
|
137
|
+
kw_values = _map_keyword(shell_args.options, remaining_kw_params)
|
|
138
|
+
|
|
139
|
+
if args:
|
|
140
|
+
log.info(
|
|
141
|
+
"Mapping shell args to function params: %s -> %s -> %s(*%s, **%s)",
|
|
142
|
+
args,
|
|
143
|
+
shell_args,
|
|
144
|
+
func.__name__,
|
|
145
|
+
pos_values,
|
|
146
|
+
kw_values,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
return func(*pos_values, **kw_values)
|
|
150
|
+
|
|
151
|
+
return wrapped
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
## Tests
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def test_wrap_function():
|
|
158
|
+
def func1(
|
|
159
|
+
arg1: str, arg2: str, arg3: int, option_one: bool = False, option_two: str | None = None
|
|
160
|
+
) -> list:
|
|
161
|
+
return [arg1, arg2, arg3, option_one, option_two]
|
|
162
|
+
|
|
163
|
+
def func2(*paths: str, summary: bool | None = False, iso_time: bool | None = False) -> list:
|
|
164
|
+
return [paths, summary, iso_time]
|
|
165
|
+
|
|
166
|
+
def func3(arg1: str, **keywords) -> list:
|
|
167
|
+
return [arg1, keywords]
|
|
168
|
+
|
|
169
|
+
def func4() -> list:
|
|
170
|
+
return []
|
|
171
|
+
|
|
172
|
+
wrapped_func1 = wrap_for_shell_args(func1)
|
|
173
|
+
wrapped_func2 = wrap_for_shell_args(func2)
|
|
174
|
+
wrapped_func3 = wrap_for_shell_args(func3)
|
|
175
|
+
wrapped_func4 = wrap_for_shell_args(func4)
|
|
176
|
+
|
|
177
|
+
print("\nwrapped:")
|
|
178
|
+
print(
|
|
179
|
+
wrapped_func1(["arg1_value", "arg2_value", "99", "--option_one", "--option_two=some_value"])
|
|
180
|
+
)
|
|
181
|
+
print(wrapped_func2(["--summary", "--iso_time", "path1", "path2", "path3"]))
|
|
182
|
+
print(wrapped_func3(["arg1_value", "--extra_param=some_value"]))
|
|
183
|
+
|
|
184
|
+
print(wrapped_func4([]))
|
|
185
|
+
|
|
186
|
+
assert wrapped_func1(
|
|
187
|
+
["arg1_value", "arg2_value", "99", "--option_one", "--option_two=some_value"]
|
|
188
|
+
) == [
|
|
189
|
+
"arg1_value",
|
|
190
|
+
"arg2_value",
|
|
191
|
+
99,
|
|
192
|
+
True,
|
|
193
|
+
"some_value",
|
|
194
|
+
]
|
|
195
|
+
assert wrapped_func2(["--summary", "--iso_time", "path1", "path2", "path3"]) == [
|
|
196
|
+
("path1", "path2", "path3"),
|
|
197
|
+
True,
|
|
198
|
+
True,
|
|
199
|
+
]
|
|
200
|
+
assert wrapped_func3(["arg1_value", "--extra_param=some_value"]) == [
|
|
201
|
+
"arg1_value",
|
|
202
|
+
{"extra_param": "some_value"},
|
|
203
|
+
]
|
|
204
|
+
assert wrapped_func4([]) == []
|