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.
Files changed (151) hide show
  1. kash/actions/__init__.py +4 -4
  2. kash/actions/core/format_markdown_template.py +2 -5
  3. kash/actions/core/markdownify.py +7 -6
  4. kash/actions/core/readability.py +7 -6
  5. kash/actions/core/render_as_html.py +37 -0
  6. kash/actions/core/show_webpage.py +6 -11
  7. kash/actions/core/strip_html.py +2 -6
  8. kash/actions/core/tabbed_webpage_config.py +31 -0
  9. kash/actions/core/{webpage_generate.py → tabbed_webpage_generate.py} +5 -4
  10. kash/commands/__init__.py +8 -20
  11. kash/commands/base/basic_file_commands.py +15 -0
  12. kash/commands/base/debug_commands.py +13 -0
  13. kash/commands/base/files_command.py +28 -10
  14. kash/commands/base/general_commands.py +21 -16
  15. kash/commands/base/logs_commands.py +4 -2
  16. kash/commands/base/model_commands.py +8 -8
  17. kash/commands/base/search_command.py +3 -2
  18. kash/commands/base/show_command.py +5 -3
  19. kash/commands/extras/parse_uv_lock.py +186 -0
  20. kash/commands/help/doc_commands.py +2 -31
  21. kash/commands/help/welcome.py +33 -0
  22. kash/commands/workspace/selection_commands.py +11 -6
  23. kash/commands/workspace/workspace_commands.py +19 -17
  24. kash/config/colors.py +3 -1
  25. kash/config/env_settings.py +14 -1
  26. kash/config/init.py +2 -2
  27. kash/config/logger.py +59 -56
  28. kash/config/logger_basic.py +3 -3
  29. kash/config/settings.py +116 -57
  30. kash/config/setup.py +28 -12
  31. kash/config/text_styles.py +3 -13
  32. kash/docs/load_api_docs.py +2 -1
  33. kash/docs/markdown/topics/a3_getting_started.md +3 -2
  34. kash/{concepts → embeddings}/text_similarity.py +2 -2
  35. kash/exec/__init__.py +20 -3
  36. kash/exec/action_decorators.py +24 -10
  37. kash/exec/action_exec.py +41 -23
  38. kash/exec/action_registry.py +13 -48
  39. kash/exec/command_registry.py +2 -1
  40. kash/exec/fetch_url_metadata.py +4 -6
  41. kash/exec/importing.py +56 -0
  42. kash/exec/llm_transforms.py +12 -10
  43. kash/exec/precondition_registry.py +2 -1
  44. kash/exec/preconditions.py +22 -1
  45. kash/exec/resolve_args.py +4 -0
  46. kash/exec/shell_callable_action.py +33 -19
  47. kash/file_storage/file_store.py +42 -27
  48. kash/file_storage/item_file_format.py +5 -2
  49. kash/file_storage/metadata_dirs.py +11 -2
  50. kash/help/assistant.py +1 -1
  51. kash/help/assistant_instructions.py +2 -1
  52. kash/help/function_param_info.py +1 -1
  53. kash/help/help_embeddings.py +2 -2
  54. kash/help/help_printing.py +7 -11
  55. kash/llm_utils/clean_headings.py +1 -1
  56. kash/llm_utils/llm_api_keys.py +4 -4
  57. kash/llm_utils/llm_features.py +68 -0
  58. kash/llm_utils/llm_messages.py +1 -2
  59. kash/llm_utils/llm_names.py +1 -1
  60. kash/llm_utils/llms.py +8 -3
  61. kash/local_server/__init__.py +5 -2
  62. kash/local_server/local_server.py +8 -5
  63. kash/local_server/local_server_commands.py +2 -2
  64. kash/local_server/local_server_routes.py +1 -7
  65. kash/local_server/local_url_formatters.py +1 -1
  66. kash/mcp/__init__.py +5 -2
  67. kash/mcp/mcp_cli.py +5 -5
  68. kash/mcp/mcp_server_commands.py +5 -5
  69. kash/mcp/mcp_server_routes.py +5 -5
  70. kash/mcp/mcp_server_sse.py +4 -2
  71. kash/media_base/media_cache.py +8 -8
  72. kash/media_base/media_services.py +1 -1
  73. kash/media_base/media_tools.py +6 -6
  74. kash/media_base/services/local_file_media.py +2 -2
  75. kash/media_base/{speech_transcription.py → transcription_deepgram.py} +25 -110
  76. kash/media_base/transcription_format.py +73 -0
  77. kash/media_base/transcription_whisper.py +38 -0
  78. kash/model/__init__.py +73 -5
  79. kash/model/actions_model.py +38 -4
  80. kash/model/concept_model.py +30 -0
  81. kash/model/items_model.py +115 -32
  82. kash/model/params_model.py +24 -0
  83. kash/shell/completions/completion_scoring.py +37 -5
  84. kash/shell/output/kerm_codes.py +1 -2
  85. kash/shell/output/shell_formatting.py +14 -4
  86. kash/shell/shell_main.py +2 -2
  87. kash/shell/utils/exception_printing.py +6 -0
  88. kash/shell/utils/native_utils.py +26 -20
  89. kash/shell/utils/shell_function_wrapper.py +15 -15
  90. kash/text_handling/custom_sliding_transforms.py +12 -4
  91. kash/text_handling/doc_normalization.py +6 -2
  92. kash/text_handling/markdown_render.py +118 -0
  93. kash/text_handling/markdown_utils.py +226 -0
  94. kash/utils/common/function_inspect.py +360 -110
  95. kash/utils/common/import_utils.py +12 -3
  96. kash/utils/common/type_utils.py +0 -29
  97. kash/utils/common/url.py +27 -3
  98. kash/utils/errors.py +6 -0
  99. kash/utils/file_utils/file_ext.py +4 -0
  100. kash/utils/file_utils/file_formats.py +2 -2
  101. kash/utils/file_utils/file_formats_model.py +20 -1
  102. kash/web_content/dir_store.py +1 -2
  103. kash/web_content/file_cache_utils.py +37 -10
  104. kash/web_content/file_processing.py +68 -0
  105. kash/web_content/local_file_cache.py +12 -9
  106. kash/web_content/web_extract.py +8 -3
  107. kash/web_content/web_fetch.py +12 -4
  108. kash/web_gen/__init__.py +0 -4
  109. kash/web_gen/simple_webpage.py +52 -0
  110. kash/web_gen/tabbed_webpage.py +24 -14
  111. kash/web_gen/template_render.py +37 -2
  112. kash/web_gen/templates/base_styles.css.jinja +169 -43
  113. kash/web_gen/templates/base_webpage.html.jinja +110 -45
  114. kash/web_gen/templates/content_styles.css.jinja +4 -2
  115. kash/web_gen/templates/item_view.html.jinja +49 -39
  116. kash/web_gen/templates/simple_webpage.html.jinja +24 -0
  117. kash/web_gen/templates/tabbed_webpage.html.jinja +42 -33
  118. kash/workspaces/__init__.py +15 -2
  119. kash/workspaces/selections.py +18 -3
  120. kash/workspaces/source_items.py +0 -1
  121. kash/workspaces/workspaces.py +5 -11
  122. kash/xonsh_custom/command_nl_utils.py +40 -19
  123. kash/xonsh_custom/custom_shell.py +43 -11
  124. kash/xonsh_custom/customize_prompt.py +39 -21
  125. kash/xonsh_custom/load_into_xonsh.py +22 -25
  126. kash/xonsh_custom/shell_load_commands.py +2 -2
  127. kash/xonsh_custom/xonsh_completers.py +2 -249
  128. kash/xonsh_custom/xonsh_keybindings.py +282 -0
  129. kash/xonsh_custom/xonsh_modern_tools.py +3 -3
  130. kash/xontrib/kash_extension.py +5 -6
  131. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/METADATA +10 -8
  132. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/RECORD +137 -136
  133. kash/actions/core/webpage_config.py +0 -21
  134. kash/concepts/concept_formats.py +0 -23
  135. kash/shell/clideps/api_keys.py +0 -100
  136. kash/shell/clideps/dotenv_setup.py +0 -115
  137. kash/shell/clideps/dotenv_utils.py +0 -98
  138. kash/shell/clideps/pkg_deps.py +0 -257
  139. kash/shell/clideps/platforms.py +0 -11
  140. kash/shell/clideps/terminal_features.py +0 -56
  141. kash/shell/utils/osc_utils.py +0 -95
  142. kash/shell/utils/terminal_images.py +0 -133
  143. kash/text_handling/markdown_util.py +0 -167
  144. kash/utils/common/atomic_var.py +0 -171
  145. kash/utils/common/string_replace.py +0 -93
  146. kash/utils/common/string_template.py +0 -101
  147. /kash/{concepts → embeddings}/cosine.py +0 -0
  148. /kash/{concepts → embeddings}/embeddings.py +0 -0
  149. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/WHEEL +0 -0
  150. {kash_shell-0.3.9.dist-info → kash_shell-0.3.11.dist-info}/entry_points.txt +0 -0
  151. {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
@@ -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)
@@ -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)