kash-shell 0.3.9__py3-none-any.whl → 0.3.11__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/actions/__init__.py +4 -4
- kash/actions/core/format_markdown_template.py +2 -5
- kash/actions/core/markdownify.py +7 -6
- kash/actions/core/readability.py +7 -6
- kash/actions/core/render_as_html.py +37 -0
- kash/actions/core/show_webpage.py +6 -11
- kash/actions/core/strip_html.py +2 -6
- kash/actions/core/tabbed_webpage_config.py +31 -0
- kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -4
- kash/commands/__init__.py +8 -20
- kash/commands/base/basic_file_commands.py +15 -0
- kash/commands/base/debug_commands.py +13 -0
- kash/commands/base/files_command.py +28 -10
- kash/commands/base/general_commands.py +21 -16
- kash/commands/base/logs_commands.py +4 -2
- kash/commands/base/model_commands.py +8 -8
- kash/commands/base/search_command.py +3 -2
- kash/commands/base/show_command.py +5 -3
- kash/commands/extras/parse_uv_lock.py +186 -0
- kash/commands/help/doc_commands.py +2 -31
- kash/commands/help/welcome.py +33 -0
- kash/commands/workspace/selection_commands.py +11 -6
- kash/commands/workspace/workspace_commands.py +19 -17
- kash/config/colors.py +3 -1
- kash/config/env_settings.py +14 -1
- kash/config/init.py +2 -2
- kash/config/logger.py +59 -56
- kash/config/logger_basic.py +3 -3
- kash/config/settings.py +116 -57
- kash/config/setup.py +28 -12
- kash/config/text_styles.py +3 -13
- kash/docs/load_api_docs.py +2 -1
- kash/docs/markdown/topics/a3_getting_started.md +3 -2
- kash/{concepts → embeddings}/text_similarity.py +2 -2
- kash/exec/__init__.py +20 -3
- kash/exec/action_decorators.py +24 -10
- kash/exec/action_exec.py +41 -23
- kash/exec/action_registry.py +13 -48
- kash/exec/command_registry.py +2 -1
- kash/exec/fetch_url_metadata.py +4 -6
- kash/exec/importing.py +56 -0
- kash/exec/llm_transforms.py +12 -10
- kash/exec/precondition_registry.py +2 -1
- kash/exec/preconditions.py +22 -1
- kash/exec/resolve_args.py +4 -0
- kash/exec/shell_callable_action.py +33 -19
- kash/file_storage/file_store.py +42 -27
- kash/file_storage/item_file_format.py +5 -2
- kash/file_storage/metadata_dirs.py +11 -2
- kash/help/assistant.py +1 -1
- kash/help/assistant_instructions.py +2 -1
- kash/help/function_param_info.py +1 -1
- kash/help/help_embeddings.py +2 -2
- kash/help/help_printing.py +7 -11
- kash/llm_utils/clean_headings.py +1 -1
- kash/llm_utils/llm_api_keys.py +4 -4
- kash/llm_utils/llm_features.py +68 -0
- kash/llm_utils/llm_messages.py +1 -2
- kash/llm_utils/llm_names.py +1 -1
- kash/llm_utils/llms.py +8 -3
- kash/local_server/__init__.py +5 -2
- kash/local_server/local_server.py +8 -5
- kash/local_server/local_server_commands.py +2 -2
- kash/local_server/local_server_routes.py +1 -7
- kash/local_server/local_url_formatters.py +1 -1
- kash/mcp/__init__.py +5 -2
- kash/mcp/mcp_cli.py +5 -5
- kash/mcp/mcp_server_commands.py +5 -5
- kash/mcp/mcp_server_routes.py +5 -5
- kash/mcp/mcp_server_sse.py +4 -2
- kash/media_base/media_cache.py +8 -8
- kash/media_base/media_services.py +1 -1
- kash/media_base/media_tools.py +6 -6
- kash/media_base/services/local_file_media.py +2 -2
- kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -110
- kash/media_base/transcription_format.py +73 -0
- kash/media_base/transcription_whisper.py +38 -0
- kash/model/__init__.py +73 -5
- kash/model/actions_model.py +38 -4
- kash/model/concept_model.py +30 -0
- kash/model/items_model.py +115 -32
- kash/model/params_model.py +24 -0
- kash/shell/completions/completion_scoring.py +37 -5
- kash/shell/output/kerm_codes.py +1 -2
- kash/shell/output/shell_formatting.py +14 -4
- kash/shell/shell_main.py +2 -2
- kash/shell/utils/exception_printing.py +6 -0
- kash/shell/utils/native_utils.py +26 -20
- kash/shell/utils/shell_function_wrapper.py +15 -15
- kash/text_handling/custom_sliding_transforms.py +12 -4
- kash/text_handling/doc_normalization.py +6 -2
- kash/text_handling/markdown_render.py +118 -0
- kash/text_handling/markdown_utils.py +226 -0
- kash/utils/common/function_inspect.py +360 -110
- kash/utils/common/import_utils.py +12 -3
- kash/utils/common/type_utils.py +0 -29
- kash/utils/common/url.py +27 -3
- kash/utils/errors.py +6 -0
- kash/utils/file_utils/file_ext.py +4 -0
- kash/utils/file_utils/file_formats.py +2 -2
- kash/utils/file_utils/file_formats_model.py +20 -1
- kash/web_content/dir_store.py +1 -2
- kash/web_content/file_cache_utils.py +37 -10
- kash/web_content/file_processing.py +68 -0
- kash/web_content/local_file_cache.py +12 -9
- kash/web_content/web_extract.py +8 -3
- kash/web_content/web_fetch.py +12 -4
- kash/web_gen/__init__.py +0 -4
- kash/web_gen/simple_webpage.py +52 -0
- kash/web_gen/tabbed_webpage.py +24 -14
- kash/web_gen/template_render.py +37 -2
- kash/web_gen/templates/base_styles.css.jinja +169 -43
- kash/web_gen/templates/base_webpage.html.jinja +110 -45
- kash/web_gen/templates/content_styles.css.jinja +4 -2
- kash/web_gen/templates/item_view.html.jinja +49 -39
- kash/web_gen/templates/simple_webpage.html.jinja +24 -0
- kash/web_gen/templates/tabbed_webpage.html.jinja +42 -33
- kash/workspaces/__init__.py +15 -2
- kash/workspaces/selections.py +18 -3
- kash/workspaces/source_items.py +0 -1
- kash/workspaces/workspaces.py +5 -11
- kash/xonsh_custom/command_nl_utils.py +40 -19
- kash/xonsh_custom/custom_shell.py +43 -11
- kash/xonsh_custom/customize_prompt.py +39 -21
- kash/xonsh_custom/load_into_xonsh.py +22 -25
- kash/xonsh_custom/shell_load_commands.py +2 -2
- kash/xonsh_custom/xonsh_completers.py +2 -249
- kash/xonsh_custom/xonsh_keybindings.py +282 -0
- kash/xonsh_custom/xonsh_modern_tools.py +3 -3
- kash/xontrib/kash_extension.py +5 -6
- {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/METADATA +10 -8
- {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/RECORD +137 -136
- kash/actions/core/webpage_config.py +0 -21
- kash/concepts/concept_formats.py +0 -23
- kash/shell/clideps/api_keys.py +0 -100
- kash/shell/clideps/dotenv_setup.py +0 -115
- kash/shell/clideps/dotenv_utils.py +0 -98
- kash/shell/clideps/pkg_deps.py +0 -257
- kash/shell/clideps/platforms.py +0 -11
- kash/shell/clideps/terminal_features.py +0 -56
- kash/shell/utils/osc_utils.py +0 -95
- kash/shell/utils/terminal_images.py +0 -133
- kash/text_handling/markdown_util.py +0 -167
- kash/utils/common/atomic_var.py +0 -171
- kash/utils/common/string_replace.py +0 -93
- kash/utils/common/string_template.py +0 -101
- /kash/{concepts → embeddings}/cosine.py +0 -0
- /kash/{concepts → embeddings}/embeddings.py +0 -0
- {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/entry_points.txt +0 -0
- {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
from pathlib import Path
|
|
2
|
-
|
|
3
|
-
from flowmark import Wrap
|
|
4
|
-
from prettyfmt import fmt_lines
|
|
5
|
-
from strif import abbrev_str
|
|
6
|
-
|
|
7
|
-
from kash.config.settings import get_system_config_dir, get_system_env_path
|
|
8
|
-
from kash.shell.clideps.dotenv_utils import (
|
|
9
|
-
env_var_is_set,
|
|
10
|
-
find_dotenv_paths,
|
|
11
|
-
read_dotenv_file,
|
|
12
|
-
update_env_file,
|
|
13
|
-
)
|
|
14
|
-
from kash.shell.input.input_prompts import input_confirm, input_simple_string
|
|
15
|
-
from kash.shell.output.shell_formatting import format_failure, format_success
|
|
16
|
-
from kash.shell.output.shell_output import (
|
|
17
|
-
cprint,
|
|
18
|
-
print_h2,
|
|
19
|
-
print_status,
|
|
20
|
-
)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def interactive_dotenv_setup(
|
|
24
|
-
api_keys: list[str],
|
|
25
|
-
update: bool = False,
|
|
26
|
-
) -> None:
|
|
27
|
-
"""
|
|
28
|
-
Interactively configure your .env file with the requested API key
|
|
29
|
-
environment variables.
|
|
30
|
-
|
|
31
|
-
:param all: Configure all known API keys (instead of just recommended ones).
|
|
32
|
-
:param update: Update values even if they are already set.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
|
-
if not update:
|
|
36
|
-
api_keys = [key for key in api_keys if not env_var_is_set(key)]
|
|
37
|
-
|
|
38
|
-
cprint()
|
|
39
|
-
print_h2("Configuring .env file")
|
|
40
|
-
if api_keys:
|
|
41
|
-
cprint(format_failure(f"API keys needed: {', '.join(api_keys)}"))
|
|
42
|
-
interactive_update_dotenv(api_keys)
|
|
43
|
-
else:
|
|
44
|
-
cprint(format_success("All requested API keys are set!"))
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def interactive_update_dotenv(keys: list[str]) -> bool:
|
|
48
|
-
"""
|
|
49
|
-
Interactively fill missing values in the active .env file.
|
|
50
|
-
Returns True if the user made changes, False otherwise.
|
|
51
|
-
"""
|
|
52
|
-
dotenv_paths = find_dotenv_paths(True, get_system_config_dir())
|
|
53
|
-
dotenv_path = dotenv_paths[0] if dotenv_paths else get_system_env_path()
|
|
54
|
-
|
|
55
|
-
if dotenv_paths:
|
|
56
|
-
print_status(f"Found .env file you will update: {dotenv_path}")
|
|
57
|
-
old_dotenv = read_dotenv_file(dotenv_path)
|
|
58
|
-
if old_dotenv:
|
|
59
|
-
cprint("Current values:")
|
|
60
|
-
summary = fmt_lines(
|
|
61
|
-
[f"{k} = {repr(abbrev_str(v or '', 12))}" for k, v in old_dotenv.items()]
|
|
62
|
-
)
|
|
63
|
-
cprint(f"File has {len(old_dotenv)} keys:\n{summary}", text_wrap=Wrap.NONE)
|
|
64
|
-
else:
|
|
65
|
-
print_status("No .env file found.")
|
|
66
|
-
|
|
67
|
-
if input_confirm(
|
|
68
|
-
"Do you want make updates to your .env file?",
|
|
69
|
-
instruction="This will leave existing keys intact unless you choose to update them.",
|
|
70
|
-
default=True,
|
|
71
|
-
):
|
|
72
|
-
dotenv_path_str = input_simple_string("Path to the .env file: ", default=str(dotenv_path))
|
|
73
|
-
if not dotenv_path_str:
|
|
74
|
-
print_status("Config changes cancelled.")
|
|
75
|
-
return False
|
|
76
|
-
|
|
77
|
-
dotenv_path = Path(dotenv_path_str)
|
|
78
|
-
|
|
79
|
-
cprint()
|
|
80
|
-
cprint(
|
|
81
|
-
"We will update the following keys from %s:\n%s",
|
|
82
|
-
dotenv_path,
|
|
83
|
-
fmt_lines(keys),
|
|
84
|
-
text_wrap=Wrap.NONE,
|
|
85
|
-
)
|
|
86
|
-
cprint()
|
|
87
|
-
cprint(
|
|
88
|
-
"Enter values for each key, or press enter to skip changes for that key. Values need not be quoted."
|
|
89
|
-
)
|
|
90
|
-
|
|
91
|
-
updates = {}
|
|
92
|
-
for key in keys:
|
|
93
|
-
value = input_simple_string(
|
|
94
|
-
f"Enter value for {key}:",
|
|
95
|
-
instruction='Leave empty to skip, use "" for a true empty string.',
|
|
96
|
-
)
|
|
97
|
-
if value and value.strip():
|
|
98
|
-
updates[key] = value
|
|
99
|
-
else:
|
|
100
|
-
cprint(f"Skipping {key}. Will not change this key.")
|
|
101
|
-
|
|
102
|
-
# Actually save the collected variables to the .env file
|
|
103
|
-
update_env_file(dotenv_path, updates, create_if_missing=True)
|
|
104
|
-
cprint()
|
|
105
|
-
cprint(format_success(f"{len(updates)} API keys saved to {dotenv_path}"))
|
|
106
|
-
cprint()
|
|
107
|
-
cprint(
|
|
108
|
-
"You can always edit the .env file directly if you need to, or "
|
|
109
|
-
"rerun `self_configure` to update your configs again."
|
|
110
|
-
)
|
|
111
|
-
else:
|
|
112
|
-
print_status("Config changes cancelled.")
|
|
113
|
-
return False
|
|
114
|
-
|
|
115
|
-
return True
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
from shutil import copyfile
|
|
4
|
-
|
|
5
|
-
from dotenv import find_dotenv, load_dotenv
|
|
6
|
-
from dotenv.main import DotEnv, rewrite, with_warn_for_invalid_lines
|
|
7
|
-
from dotenv.parser import parse_stream
|
|
8
|
-
|
|
9
|
-
DOTENV_NAMES = [".env", ".env.local"]
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
def find_dotenv_paths(include_home: bool = True, *extra_dirs: Path) -> list[Path]:
|
|
13
|
-
"""
|
|
14
|
-
Find .env files in the current directory and return a list of paths.
|
|
15
|
-
If extra_dirs are provided, they will be checked for .env files as well.
|
|
16
|
-
"""
|
|
17
|
-
paths = [find_dotenv(filename=path, usecwd=True) for path in DOTENV_NAMES]
|
|
18
|
-
if include_home:
|
|
19
|
-
paths.append("~")
|
|
20
|
-
for dir in extra_dirs:
|
|
21
|
-
for path in DOTENV_NAMES:
|
|
22
|
-
if (dir / path).expanduser().exists():
|
|
23
|
-
paths.append(str(dir / path))
|
|
24
|
-
return [Path(path).expanduser().resolve() for path in paths if path]
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def load_dotenv_paths(
|
|
28
|
-
override: bool = True, include_home: bool = True, *extra_dirs: Path
|
|
29
|
-
) -> list[Path]:
|
|
30
|
-
"""
|
|
31
|
-
Find and load .env files.
|
|
32
|
-
"""
|
|
33
|
-
dotenv_paths = find_dotenv_paths(include_home, *extra_dirs)
|
|
34
|
-
for dotenv_path in dotenv_paths:
|
|
35
|
-
load_dotenv(dotenv_path, override=override)
|
|
36
|
-
return dotenv_paths
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def read_dotenv_file(dotenv_path: str | Path) -> dict[str, str | None]:
|
|
40
|
-
"""
|
|
41
|
-
Read a .env file and return a dictionary of key-value pairs.
|
|
42
|
-
"""
|
|
43
|
-
return DotEnv(dotenv_path=dotenv_path).dict()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def env_var_is_set(key: str, min_length: int = 10, forbidden_str: str = "changeme") -> bool:
|
|
47
|
-
"""
|
|
48
|
-
Check if an environment variable is set and plausible (not a dummy or empty value).
|
|
49
|
-
"""
|
|
50
|
-
value = os.environ.get(key, None)
|
|
51
|
-
return bool(value and len(value.strip()) > min_length and forbidden_str not in value)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def update_env_file(
|
|
55
|
-
dotenv_path: Path,
|
|
56
|
-
updates: dict[str, str],
|
|
57
|
-
create_if_missing: bool = False,
|
|
58
|
-
backup_suffix: str | None = ".bak",
|
|
59
|
-
) -> tuple[list[str], list[str]]:
|
|
60
|
-
"""
|
|
61
|
-
Updates values in a .env file (safely). Similar to what dotenv offers but allows multiple
|
|
62
|
-
updates at once and keeps a backup. Values may be quoted or unquoted.
|
|
63
|
-
"""
|
|
64
|
-
if not create_if_missing and not dotenv_path.exists():
|
|
65
|
-
raise FileNotFoundError(f".env file does not exist: {dotenv_path}")
|
|
66
|
-
|
|
67
|
-
# Create the .env file directory if it doesn't exist
|
|
68
|
-
if create_if_missing and not dotenv_path.parent.exists():
|
|
69
|
-
dotenv_path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
-
|
|
71
|
-
def format_line(key: str, value: str) -> str:
|
|
72
|
-
if (value.startswith("'") and value.endswith("'")) or (
|
|
73
|
-
value.startswith('"') and value.endswith('"')
|
|
74
|
-
):
|
|
75
|
-
return f"{key}={value}"
|
|
76
|
-
else:
|
|
77
|
-
return f"{key}=" + '"' + value.replace('"', '\\"') + '"'
|
|
78
|
-
|
|
79
|
-
if backup_suffix and dotenv_path.exists():
|
|
80
|
-
copyfile(dotenv_path, dotenv_path.with_name(dotenv_path.name + backup_suffix))
|
|
81
|
-
|
|
82
|
-
changed = []
|
|
83
|
-
added = []
|
|
84
|
-
with rewrite(dotenv_path, encoding="utf-8") as (source, dest):
|
|
85
|
-
for mapping in with_warn_for_invalid_lines(parse_stream(source)):
|
|
86
|
-
if mapping.key in updates:
|
|
87
|
-
dest.write(format_line(mapping.key, updates[mapping.key]))
|
|
88
|
-
dest.write("\n")
|
|
89
|
-
changed.append(mapping.key)
|
|
90
|
-
else:
|
|
91
|
-
dest.write(mapping.original.string.rstrip("\n"))
|
|
92
|
-
dest.write("\n")
|
|
93
|
-
for key in set(updates.keys()) - set(changed):
|
|
94
|
-
dest.write(format_line(key, updates[key]))
|
|
95
|
-
dest.write("\n")
|
|
96
|
-
added.append(key)
|
|
97
|
-
|
|
98
|
-
return changed, added
|
kash/shell/clideps/pkg_deps.py
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import logging
|
|
2
|
-
import shutil
|
|
3
|
-
from collections.abc import Callable
|
|
4
|
-
from dataclasses import dataclass
|
|
5
|
-
from enum import Enum
|
|
6
|
-
|
|
7
|
-
from cachetools import TTLCache, cached
|
|
8
|
-
from rich.console import Group
|
|
9
|
-
from rich.text import Text
|
|
10
|
-
|
|
11
|
-
from kash.config.text_styles import EMOJI_WARN
|
|
12
|
-
from kash.shell.clideps.platforms import PLATFORM, Platform
|
|
13
|
-
from kash.shell.output.shell_formatting import format_name_and_value, format_success_or_failure
|
|
14
|
-
from kash.shell.output.shell_output import cprint
|
|
15
|
-
from kash.utils.errors import SetupError
|
|
16
|
-
|
|
17
|
-
log = logging.getLogger(__name__)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@dataclass(frozen=True)
|
|
21
|
-
class PkgDep:
|
|
22
|
-
"""
|
|
23
|
-
Information about a system tool dependency and how to install it.
|
|
24
|
-
"""
|
|
25
|
-
|
|
26
|
-
command_names: tuple[str, ...]
|
|
27
|
-
check_function: Callable[[], bool] | None = None
|
|
28
|
-
comment: str | None = None
|
|
29
|
-
warn_if_missing: bool = False
|
|
30
|
-
|
|
31
|
-
brew_pkg: str | None = None
|
|
32
|
-
apt_pkg: str | None = None
|
|
33
|
-
pixi_pkg: str | None = None
|
|
34
|
-
pip_pkg: str | None = None
|
|
35
|
-
winget_pkg: str | None = None
|
|
36
|
-
chocolatey_pkg: str | None = None
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def check_libmagic():
|
|
40
|
-
try:
|
|
41
|
-
import magic
|
|
42
|
-
|
|
43
|
-
magic.Magic()
|
|
44
|
-
return True
|
|
45
|
-
except Exception as e:
|
|
46
|
-
log.info("libmagic is not installed or not accessible: %s", e)
|
|
47
|
-
return False
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class Pkg(Enum):
|
|
51
|
-
"""
|
|
52
|
-
Specific external packages (like libraries and system tools) that are
|
|
53
|
-
often useful, especially from within Python or a shell.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
# These are usually pre-installed on all platforms:
|
|
57
|
-
less = PkgDep(("less",))
|
|
58
|
-
tail = PkgDep(("tail",))
|
|
59
|
-
|
|
60
|
-
bat = PkgDep(
|
|
61
|
-
("batcat", "bat"), # batcat for Debian/Ubuntu), bat for macOS
|
|
62
|
-
comment="Not available on ubuntu, but in pixi",
|
|
63
|
-
brew_pkg="bat",
|
|
64
|
-
pixi_pkg="bat",
|
|
65
|
-
winget_pkg="sharkdp.bat",
|
|
66
|
-
warn_if_missing=True,
|
|
67
|
-
)
|
|
68
|
-
ripgrep = PkgDep(
|
|
69
|
-
("rg",),
|
|
70
|
-
brew_pkg="ripgrep",
|
|
71
|
-
apt_pkg="ripgrep",
|
|
72
|
-
winget_pkg="BurntSushi.ripgrep",
|
|
73
|
-
warn_if_missing=True,
|
|
74
|
-
)
|
|
75
|
-
eza = PkgDep(
|
|
76
|
-
("eza",),
|
|
77
|
-
brew_pkg="eza",
|
|
78
|
-
apt_pkg="eza",
|
|
79
|
-
winget_pkg="eza-community.eza",
|
|
80
|
-
warn_if_missing=True,
|
|
81
|
-
)
|
|
82
|
-
zoxide = PkgDep(
|
|
83
|
-
("zoxide",),
|
|
84
|
-
brew_pkg="zoxide",
|
|
85
|
-
apt_pkg="zoxide",
|
|
86
|
-
winget_pkg="ajeetdsouza.zoxide",
|
|
87
|
-
warn_if_missing=True,
|
|
88
|
-
)
|
|
89
|
-
hexyl = PkgDep(
|
|
90
|
-
("hexyl",),
|
|
91
|
-
brew_pkg="hexyl",
|
|
92
|
-
apt_pkg="hexyl",
|
|
93
|
-
winget_pkg="sharkdp.hexyl",
|
|
94
|
-
warn_if_missing=True,
|
|
95
|
-
)
|
|
96
|
-
pygmentize = PkgDep(
|
|
97
|
-
("pygmentize",),
|
|
98
|
-
brew_pkg="pygments",
|
|
99
|
-
apt_pkg="python3-pygments",
|
|
100
|
-
pip_pkg="Pygments",
|
|
101
|
-
)
|
|
102
|
-
libmagic = PkgDep(
|
|
103
|
-
(),
|
|
104
|
-
comment="""
|
|
105
|
-
For macOS and Linux, brew or apt gives the latest binaries. For Windows, it may be
|
|
106
|
-
easier to use pip.
|
|
107
|
-
""",
|
|
108
|
-
check_function=check_libmagic,
|
|
109
|
-
brew_pkg="libmagic",
|
|
110
|
-
apt_pkg="libmagic1",
|
|
111
|
-
pip_pkg="python-magic-bin",
|
|
112
|
-
warn_if_missing=True,
|
|
113
|
-
)
|
|
114
|
-
libgl1 = PkgDep(
|
|
115
|
-
command_names=(),
|
|
116
|
-
comment="Needed on ubuntu along with ffmpeg",
|
|
117
|
-
apt_pkg="libgl1",
|
|
118
|
-
)
|
|
119
|
-
ffmpeg = PkgDep(
|
|
120
|
-
("ffmpeg",),
|
|
121
|
-
comment="Needed by yt-dlp and other essential tools",
|
|
122
|
-
brew_pkg="ffmpeg",
|
|
123
|
-
apt_pkg="ffmpeg",
|
|
124
|
-
winget_pkg="Gyan.FFmpeg",
|
|
125
|
-
warn_if_missing=True,
|
|
126
|
-
)
|
|
127
|
-
imagemagick = PkgDep(
|
|
128
|
-
("magick",),
|
|
129
|
-
brew_pkg="imagemagick",
|
|
130
|
-
apt_pkg="imagemagick",
|
|
131
|
-
winget_pkg="ImageMagick.ImageMagick",
|
|
132
|
-
warn_if_missing=True,
|
|
133
|
-
)
|
|
134
|
-
dust = PkgDep(
|
|
135
|
-
("dust",),
|
|
136
|
-
comment="Not available on ubuntu, but in pixi",
|
|
137
|
-
brew_pkg="dust",
|
|
138
|
-
pixi_pkg="dust",
|
|
139
|
-
winget_pkg="bootandy.dust",
|
|
140
|
-
warn_if_missing=True,
|
|
141
|
-
)
|
|
142
|
-
duf = PkgDep(
|
|
143
|
-
("duf",),
|
|
144
|
-
comment="Not in winget. Only in unstable on ubuntu, but in pixi.",
|
|
145
|
-
brew_pkg="duf",
|
|
146
|
-
pixi_pkg="duf",
|
|
147
|
-
chocolatey_pkg="duf",
|
|
148
|
-
)
|
|
149
|
-
|
|
150
|
-
@property
|
|
151
|
-
def full_name(self) -> str:
|
|
152
|
-
name = self.name
|
|
153
|
-
if self.value.command_names:
|
|
154
|
-
name += f" ({' or '.join(f'`{name}`' for name in self.value.command_names)})"
|
|
155
|
-
return name
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
@dataclass(frozen=True)
|
|
159
|
-
class InstalledPkgs:
|
|
160
|
-
"""
|
|
161
|
-
Info about which tools are installed.
|
|
162
|
-
"""
|
|
163
|
-
|
|
164
|
-
tools: dict[Pkg, str | bool]
|
|
165
|
-
|
|
166
|
-
def has(self, *tools: Pkg) -> bool:
|
|
167
|
-
return all(self.tools[tool] for tool in tools)
|
|
168
|
-
|
|
169
|
-
def require(self, *tools: Pkg) -> None:
|
|
170
|
-
for tool in tools:
|
|
171
|
-
if not self.has(tool):
|
|
172
|
-
print_missing_tool_help(tool)
|
|
173
|
-
raise SetupError(
|
|
174
|
-
f"`{tool.value}` ({tool.value.command_names}) needed but not found"
|
|
175
|
-
)
|
|
176
|
-
|
|
177
|
-
def missing_tools(self, *tools: Pkg) -> list[Pkg]:
|
|
178
|
-
if not tools:
|
|
179
|
-
tools = tuple(Pkg)
|
|
180
|
-
return [tool for tool in tools if not self.tools[tool]]
|
|
181
|
-
|
|
182
|
-
def warn_if_missing(self, *tools: Pkg) -> None:
|
|
183
|
-
for tool in self.missing_tools(*tools):
|
|
184
|
-
if tool.value.warn_if_missing:
|
|
185
|
-
print_missing_tool_help(tool)
|
|
186
|
-
|
|
187
|
-
def formatted(self) -> Group:
|
|
188
|
-
texts: list[Text | Group] = []
|
|
189
|
-
for tool, path in self.items():
|
|
190
|
-
found_str = "Found" if isinstance(path, bool) else f"Found: `{path}`"
|
|
191
|
-
doc = format_success_or_failure(
|
|
192
|
-
bool(path),
|
|
193
|
-
true_str=format_name_and_value(tool.name, found_str),
|
|
194
|
-
false_str=format_name_and_value(tool.name, "Not found!"),
|
|
195
|
-
)
|
|
196
|
-
texts.append(doc)
|
|
197
|
-
|
|
198
|
-
return Group(*texts)
|
|
199
|
-
|
|
200
|
-
def items(self) -> list[tuple[Pkg, str | bool]]:
|
|
201
|
-
return sorted(self.tools.items(), key=lambda item: item[0].name)
|
|
202
|
-
|
|
203
|
-
def status(self) -> Text:
|
|
204
|
-
texts: list[Text] = []
|
|
205
|
-
for tool, path in self.items():
|
|
206
|
-
texts.append(format_success_or_failure(bool(path), tool.name))
|
|
207
|
-
|
|
208
|
-
return Text.assemble("Local system tools found: ", Text(" ").join(texts))
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def print_missing_tool_help(tool: Pkg):
|
|
212
|
-
warn_str = f"{EMOJI_WARN} {tool.full_name} was not found; it is recommended to install it for better functionality."
|
|
213
|
-
if tool.value.comment:
|
|
214
|
-
warn_str += f" {tool.value.comment}"
|
|
215
|
-
install_str = get_install_suggestion(tool)
|
|
216
|
-
if install_str:
|
|
217
|
-
warn_str += f" {install_str}"
|
|
218
|
-
|
|
219
|
-
cprint(warn_str)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
def get_install_suggestion(*missing_tools: Pkg) -> str | None:
|
|
223
|
-
brew_pkgs = [tool.value.brew_pkg for tool in missing_tools if tool.value.brew_pkg]
|
|
224
|
-
apt_pkgs = [tool.value.apt_pkg for tool in missing_tools if tool.value.apt_pkg]
|
|
225
|
-
winget_pkgs = [tool.value.winget_pkg for tool in missing_tools if tool.value.winget_pkg]
|
|
226
|
-
pip_pkgs = [tool.value.pip_pkg for tool in missing_tools if tool.value.pip_pkg]
|
|
227
|
-
|
|
228
|
-
if PLATFORM == Platform.Darwin and brew_pkgs:
|
|
229
|
-
return "On macOS, try using Homebrew: `brew install %s`" % " ".join(brew_pkgs)
|
|
230
|
-
elif PLATFORM == Platform.Linux and apt_pkgs:
|
|
231
|
-
return "On Linux, try using your package manager, e.g.: `sudo apt install %s`" % " ".join(
|
|
232
|
-
apt_pkgs
|
|
233
|
-
)
|
|
234
|
-
elif PLATFORM == Platform.Windows and winget_pkgs:
|
|
235
|
-
return "On Windows, try using Winget: `winget install %s`" % " ".join(winget_pkgs)
|
|
236
|
-
|
|
237
|
-
if pip_pkgs:
|
|
238
|
-
return "You may also try using pip: `pip install %s`" % " ".join(pip_pkgs)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
@cached(TTLCache(maxsize=1, ttl=5.0))
|
|
242
|
-
def pkg_check() -> InstalledPkgs:
|
|
243
|
-
"""
|
|
244
|
-
Check which third-party tools are installed.
|
|
245
|
-
"""
|
|
246
|
-
tools: dict[Pkg, str | bool] = {}
|
|
247
|
-
|
|
248
|
-
def which_tool(tool: Pkg) -> str | None:
|
|
249
|
-
return next(filter(None, (shutil.which(name) for name in tool.value.command_names)), None)
|
|
250
|
-
|
|
251
|
-
def check_tool(tool: Pkg) -> bool:
|
|
252
|
-
return bool(tool.value.check_function and tool.value.check_function())
|
|
253
|
-
|
|
254
|
-
for tool in Pkg:
|
|
255
|
-
tools[tool] = which_tool(tool) or check_tool(tool)
|
|
256
|
-
|
|
257
|
-
return InstalledPkgs(tools)
|
kash/shell/clideps/platforms.py
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
from dataclasses import dataclass
|
|
3
|
-
|
|
4
|
-
from rich import get_console
|
|
5
|
-
from rich.text import Text
|
|
6
|
-
|
|
7
|
-
from kash.shell.output.shell_formatting import format_success_or_failure
|
|
8
|
-
from kash.shell.output.shell_output import cprint
|
|
9
|
-
from kash.shell.utils.osc_utils import osc8_link_rich, terminal_supports_osc8
|
|
10
|
-
from kash.shell.utils.terminal_images import terminal_supports_sixel
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@dataclass(frozen=True)
|
|
14
|
-
class TerminalInfo:
|
|
15
|
-
term: str
|
|
16
|
-
term_program: str
|
|
17
|
-
terminal_width: int
|
|
18
|
-
supports_sixel: bool
|
|
19
|
-
supports_osc8: bool
|
|
20
|
-
|
|
21
|
-
def as_text(self) -> Text:
|
|
22
|
-
return Text.assemble(
|
|
23
|
-
f"{self.terminal_width} cols, ",
|
|
24
|
-
format_success_or_failure(
|
|
25
|
-
self.supports_sixel, true_str="Sixel images", false_str="No Sixel images"
|
|
26
|
-
),
|
|
27
|
-
", ",
|
|
28
|
-
format_success_or_failure(
|
|
29
|
-
self.supports_osc8,
|
|
30
|
-
true_str=osc8_link_rich(
|
|
31
|
-
"https://github.com/Alhadis/OSC8-Adoption", "OSC 8 hyperlinks"
|
|
32
|
-
),
|
|
33
|
-
false_str="No OSC 8 hyperlinks",
|
|
34
|
-
),
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
def print_term_info(self):
|
|
38
|
-
cprint(
|
|
39
|
-
Text.assemble(
|
|
40
|
-
f"Terminal is {self.term} ({self.term_program}), ",
|
|
41
|
-
self.as_text(),
|
|
42
|
-
)
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def terminal_check() -> TerminalInfo:
|
|
47
|
-
"""
|
|
48
|
-
Get a summary of the current terminal's name, settings, and capabilities.
|
|
49
|
-
"""
|
|
50
|
-
return TerminalInfo(
|
|
51
|
-
term=os.environ.get("TERM", ""),
|
|
52
|
-
term_program=os.environ.get("TERM_PROGRAM", ""),
|
|
53
|
-
supports_sixel=terminal_supports_sixel(),
|
|
54
|
-
supports_osc8=terminal_supports_osc8(),
|
|
55
|
-
terminal_width=get_console().width,
|
|
56
|
-
)
|
kash/shell/utils/osc_utils.py
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
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)
|