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