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.
- kash/actions/__init__.py +4 -4
- kash/actions/core/markdownify.py +5 -2
- kash/actions/core/readability.py +5 -2
- kash/actions/core/render_as_html.py +18 -0
- kash/actions/core/webpage_config.py +12 -4
- kash/commands/__init__.py +8 -20
- kash/commands/base/basic_file_commands.py +15 -0
- kash/commands/base/debug_commands.py +15 -2
- kash/commands/base/general_commands.py +27 -18
- kash/commands/base/logs_commands.py +1 -4
- 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 -16
- kash/config/colors.py +2 -0
- kash/config/env_settings.py +72 -0
- kash/config/init.py +2 -2
- kash/config/logger.py +61 -59
- kash/config/logger_basic.py +12 -5
- kash/config/server_config.py +6 -6
- kash/config/settings.py +117 -67
- kash/config/setup.py +35 -9
- kash/config/suppress_warnings.py +30 -12
- kash/config/text_styles.py +3 -13
- kash/docs/load_api_docs.py +2 -1
- kash/docs/markdown/topics/a2_installation.md +7 -3
- kash/docs/markdown/topics/a3_getting_started.md +3 -2
- kash/docs/markdown/warning.md +3 -8
- kash/docs/markdown/welcome.md +4 -0
- kash/docs_base/load_recipe_snippets.py +1 -1
- kash/docs_base/recipes/{general_system_commands.ksh → general_system_commands.sh} +1 -1
- kash/{concepts → embeddings}/cosine.py +2 -1
- kash/embeddings/text_similarity.py +57 -0
- kash/exec/__init__.py +20 -3
- kash/exec/action_decorators.py +18 -4
- 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 +6 -6
- kash/exec/precondition_registry.py +2 -1
- kash/exec/preconditions.py +16 -1
- kash/exec/shell_callable_action.py +33 -19
- kash/file_storage/file_store.py +23 -14
- kash/file_storage/item_file_format.py +13 -3
- kash/file_storage/metadata_dirs.py +11 -2
- kash/help/assistant.py +2 -2
- kash/help/assistant_instructions.py +2 -1
- kash/help/help_embeddings.py +2 -2
- kash/help/help_printing.py +14 -10
- kash/help/tldr_help.py +5 -3
- kash/llm_utils/clean_headings.py +1 -1
- kash/llm_utils/llm_api_keys.py +4 -4
- kash/llm_utils/llm_completion.py +2 -2
- 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 +17 -12
- kash/local_server/__init__.py +5 -2
- kash/local_server/local_server.py +56 -46
- kash/local_server/local_server_commands.py +15 -15
- kash/local_server/local_server_routes.py +2 -2
- kash/local_server/local_url_formatters.py +1 -1
- kash/mcp/__init__.py +5 -2
- kash/mcp/mcp_cli.py +54 -17
- kash/mcp/mcp_server_commands.py +5 -6
- kash/mcp/mcp_server_routes.py +14 -11
- kash/mcp/mcp_server_sse.py +61 -34
- kash/mcp/mcp_server_stdio.py +0 -8
- kash/media_base/audio_processing.py +81 -7
- kash/media_base/media_cache.py +18 -18
- 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 -109
- 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 +56 -13
- 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/text_handling/custom_sliding_transforms.py +12 -4
- kash/text_handling/doc_normalization.py +6 -2
- kash/text_handling/markdown_render.py +117 -0
- kash/text_handling/markdown_utils.py +204 -0
- kash/utils/common/import_utils.py +12 -3
- kash/utils/common/type_utils.py +0 -29
- kash/utils/common/url.py +80 -28
- kash/utils/errors.py +6 -0
- kash/utils/file_utils/{dir_size.py → dir_info.py} +25 -4
- kash/utils/file_utils/file_ext.py +2 -3
- kash/utils/file_utils/file_formats.py +28 -2
- kash/utils/file_utils/file_formats_model.py +50 -19
- kash/utils/file_utils/filename_parsing.py +10 -4
- 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/tabbed_webpage.py +5 -2
- kash/web_gen/templates/base_styles.css.jinja +120 -14
- kash/web_gen/templates/base_webpage.html.jinja +60 -13
- kash/web_gen/templates/content_styles.css.jinja +4 -2
- kash/web_gen/templates/item_view.html.jinja +2 -2
- kash/web_gen/templates/tabbed_webpage.html.jinja +1 -2
- kash/workspaces/__init__.py +15 -2
- kash/workspaces/selections.py +18 -3
- kash/workspaces/source_items.py +4 -2
- kash/workspaces/workspace_output.py +11 -4
- kash/workspaces/workspaces.py +5 -11
- kash/xonsh_custom/command_nl_utils.py +40 -19
- kash/xonsh_custom/custom_shell.py +44 -12
- kash/xonsh_custom/customize_prompt.py +39 -21
- kash/xonsh_custom/load_into_xonsh.py +26 -27
- 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.8.dist-info → kash_shell-0.3.10.dist-info}/METADATA +26 -12
- {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/RECORD +140 -140
- {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/entry_points.txt +1 -1
- kash/concepts/concept_formats.py +0 -23
- kash/concepts/text_similarity.py +0 -112
- kash/shell/clideps/api_keys.py +0 -99
- kash/shell/clideps/dotenv_setup.py +0 -114
- kash/shell/clideps/dotenv_utils.py +0 -89
- kash/shell/clideps/pkg_deps.py +0 -232
- 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 -158
- kash/utils/common/string_replace.py +0 -93
- kash/utils/common/string_template.py +0 -101
- /kash/docs_base/recipes/{python_dev_commands.ksh → python_dev_commands.sh} +0 -0
- /kash/docs_base/recipes/{tldr_standard_commands.ksh → tldr_standard_commands.sh} +0 -0
- /kash/{concepts → embeddings}/embeddings.py +0 -0
- {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/WHEEL +0 -0
- {kash_shell-0.3.8.dist-info → kash_shell-0.3.10.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import NewType
|
|
2
|
+
|
|
3
|
+
from kash.utils.lang_utils.capitalization import capitalize_cms
|
|
4
|
+
|
|
5
|
+
Concept = NewType("Concept", str)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def canonicalize_concept(concept: str, capitalize: bool = True) -> Concept:
|
|
9
|
+
"""
|
|
10
|
+
Convert a concept string (general name, person, etc.) to a canonical form.
|
|
11
|
+
Drop any extraneous Markdown bullets. Drop any quoted phrases (e.g. book titles etc)
|
|
12
|
+
for consistency.
|
|
13
|
+
"""
|
|
14
|
+
concept = concept.strip("-* ")
|
|
15
|
+
for quote in ['"', "'"]:
|
|
16
|
+
if concept.startswith(quote) and concept.endswith(quote):
|
|
17
|
+
concept = concept[1:-1]
|
|
18
|
+
if capitalize:
|
|
19
|
+
return Concept(capitalize_cms(concept))
|
|
20
|
+
else:
|
|
21
|
+
return Concept(concept)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def normalize_concepts(
|
|
25
|
+
concepts: list[str], sort_dedup: bool = True, capitalize: bool = True
|
|
26
|
+
) -> list[Concept]:
|
|
27
|
+
if sort_dedup:
|
|
28
|
+
return sorted(set(canonicalize_concept(concept, capitalize) for concept in concepts))
|
|
29
|
+
else:
|
|
30
|
+
return [canonicalize_concept(concept, capitalize) for concept in concepts]
|
kash/model/items_model.py
CHANGED
|
@@ -9,17 +9,22 @@ from pathlib import Path
|
|
|
9
9
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
10
10
|
|
|
11
11
|
from frontmatter_format import from_yaml_string, new_yaml
|
|
12
|
-
from prettyfmt import
|
|
12
|
+
from prettyfmt import (
|
|
13
|
+
abbrev_obj,
|
|
14
|
+
abbrev_on_words,
|
|
15
|
+
abbrev_phrase_in_middle,
|
|
16
|
+
sanitize_title,
|
|
17
|
+
slugify_snake,
|
|
18
|
+
)
|
|
13
19
|
from pydantic.dataclasses import dataclass
|
|
14
|
-
from slugify import slugify
|
|
15
20
|
from strif import abbrev_str, format_iso_timestamp
|
|
16
21
|
|
|
17
|
-
from kash.concepts.concept_formats import canonicalize_concept
|
|
18
22
|
from kash.config.logger import get_logger
|
|
23
|
+
from kash.model.concept_model import canonicalize_concept
|
|
19
24
|
from kash.model.media_model import MediaMetadata
|
|
20
25
|
from kash.model.operations_model import OperationSummary, Source
|
|
21
26
|
from kash.model.paths_model import StorePath, fmt_store_path
|
|
22
|
-
from kash.text_handling.
|
|
27
|
+
from kash.text_handling.markdown_render import markdown_to_html
|
|
23
28
|
from kash.utils.common.format_utils import fmt_loc, html_to_plaintext, plaintext_to_html
|
|
24
29
|
from kash.utils.common.url import Locator, Url
|
|
25
30
|
from kash.utils.errors import FileFormatError
|
|
@@ -28,6 +33,7 @@ from kash.utils.file_utils.file_formats_model import FileExt, Format
|
|
|
28
33
|
|
|
29
34
|
if TYPE_CHECKING:
|
|
30
35
|
from kash.model.actions_model import ExecContext
|
|
36
|
+
from kash.workspaces import Workspace
|
|
31
37
|
|
|
32
38
|
log = get_logger(__name__)
|
|
33
39
|
|
|
@@ -77,13 +83,14 @@ class ItemType(Enum):
|
|
|
77
83
|
Format.yaml: ItemType.doc,
|
|
78
84
|
Format.diff: ItemType.doc,
|
|
79
85
|
Format.python: ItemType.extension,
|
|
80
|
-
Format.kash_script: ItemType.extension,
|
|
81
86
|
Format.json: ItemType.doc,
|
|
82
87
|
Format.csv: ItemType.doc,
|
|
83
88
|
Format.log: ItemType.log,
|
|
84
89
|
Format.pdf: ItemType.resource,
|
|
85
90
|
Format.jpeg: ItemType.asset,
|
|
86
91
|
Format.png: ItemType.asset,
|
|
92
|
+
Format.gif: ItemType.asset,
|
|
93
|
+
Format.svg: ItemType.asset,
|
|
87
94
|
Format.docx: ItemType.resource,
|
|
88
95
|
Format.mp3: ItemType.resource,
|
|
89
96
|
Format.m4a: ItemType.resource,
|
|
@@ -304,7 +311,11 @@ class Item:
|
|
|
304
311
|
key: value for key, value in item_dict.items() if key not in all_fields
|
|
305
312
|
}
|
|
306
313
|
if unexpected_metadata:
|
|
307
|
-
log.info(
|
|
314
|
+
log.info(
|
|
315
|
+
"Skipping unexpected metadata on item: %s%s",
|
|
316
|
+
info_prefix,
|
|
317
|
+
unexpected_metadata,
|
|
318
|
+
)
|
|
308
319
|
|
|
309
320
|
result = cls(
|
|
310
321
|
type=type_,
|
|
@@ -322,7 +333,10 @@ class Item:
|
|
|
322
333
|
|
|
323
334
|
@classmethod
|
|
324
335
|
def from_external_path(
|
|
325
|
-
cls,
|
|
336
|
+
cls,
|
|
337
|
+
path: Path | str,
|
|
338
|
+
item_type: ItemType | None = None,
|
|
339
|
+
title: str | None = None,
|
|
326
340
|
) -> Item:
|
|
327
341
|
"""
|
|
328
342
|
Create a resource Item for a file with a format inferred from the file extension
|
|
@@ -394,6 +408,19 @@ class Item:
|
|
|
394
408
|
if self.type.expects_body and self.format.has_body and not self.body:
|
|
395
409
|
raise ValueError(f"Item type `{self.type.value}` is text but has no body: {self}")
|
|
396
410
|
|
|
411
|
+
def absolute_path(self, ws: "Workspace | None" = None) -> Path: # noqa: UP037
|
|
412
|
+
"""
|
|
413
|
+
Get the absolute path to the item. Throws `ValueError` if the item has no
|
|
414
|
+
store path. If no workspace is provided, uses the current workspace.
|
|
415
|
+
"""
|
|
416
|
+
from kash.workspaces import current_ws
|
|
417
|
+
|
|
418
|
+
if not self.store_path:
|
|
419
|
+
raise ValueError("Item has no store path")
|
|
420
|
+
if not ws:
|
|
421
|
+
ws = current_ws()
|
|
422
|
+
return ws.base_dir / self.store_path
|
|
423
|
+
|
|
397
424
|
@property
|
|
398
425
|
def is_binary(self) -> bool:
|
|
399
426
|
return bool(self.format and self.format.is_binary)
|
|
@@ -535,13 +562,20 @@ class Item:
|
|
|
535
562
|
|
|
536
563
|
return body_text[:max_len]
|
|
537
564
|
|
|
565
|
+
@property
|
|
566
|
+
def has_body(self) -> bool:
|
|
567
|
+
"""
|
|
568
|
+
True if the item has a non-empty body.
|
|
569
|
+
"""
|
|
570
|
+
return bool(self.body and self.body.strip())
|
|
571
|
+
|
|
538
572
|
def slug_name(self, max_len: int = SLUG_MAX_LEN) -> str:
|
|
539
573
|
"""
|
|
540
574
|
Get a readable slugified version of the title or filename or content
|
|
541
575
|
appropriate for this item. May not be unique.
|
|
542
576
|
"""
|
|
543
577
|
title = self.abbrev_title(max_len=max_len)
|
|
544
|
-
slug =
|
|
578
|
+
slug = slugify_snake(title)
|
|
545
579
|
return slug
|
|
546
580
|
|
|
547
581
|
def abbrev_description(self, max_len: int = 1000) -> str:
|
|
@@ -584,8 +618,8 @@ class Item:
|
|
|
584
618
|
# Python files cannot have more than one . in them.
|
|
585
619
|
return f"{FileExt.py.value}"
|
|
586
620
|
elif self.type == ItemType.script:
|
|
587
|
-
# Same for kash scripts.
|
|
588
|
-
return f"{self.type.value}.{FileExt.
|
|
621
|
+
# Same for kash/xonsh scripts.
|
|
622
|
+
return f"{self.type.value}.{FileExt.xsh.value}"
|
|
589
623
|
else:
|
|
590
624
|
return f"{self.type.value}.{self.get_file_ext().value}"
|
|
591
625
|
|
|
@@ -675,6 +709,10 @@ class Item:
|
|
|
675
709
|
updates = other_updates.copy()
|
|
676
710
|
updates["type"] = type
|
|
677
711
|
|
|
712
|
+
# If format was specified and user didn't specify file_ext, then infer it.
|
|
713
|
+
if "file_ext" not in other_updates and "format" in other_updates:
|
|
714
|
+
updates["file_ext"] = other_updates["format"].file_ext
|
|
715
|
+
|
|
678
716
|
# External resource paths only make sense for resources, so clear them out if new item
|
|
679
717
|
# is not a resource.
|
|
680
718
|
new_type = updates.get("type") or self.type
|
|
@@ -685,15 +723,20 @@ class Item:
|
|
|
685
723
|
if derived_from:
|
|
686
724
|
new_item.update_relations(derived_from=derived_from)
|
|
687
725
|
|
|
688
|
-
# Fall back to action title template if we have it and
|
|
726
|
+
# Fall back to action title template if we have it and title wasn't explicitly set.
|
|
689
727
|
if "title" not in other_updates:
|
|
728
|
+
prev_title = self.title or (Path(self.store_path).stem if self.store_path else UNTITLED)
|
|
690
729
|
if self.context:
|
|
691
730
|
action = self.context.action
|
|
692
731
|
new_item.title = action.title_template.format(
|
|
693
|
-
title=
|
|
732
|
+
title=prev_title, action_name=action.name
|
|
694
733
|
)
|
|
695
734
|
else:
|
|
696
|
-
log.warning(
|
|
735
|
+
log.warning(
|
|
736
|
+
"Deriving an item without action context so keeping previous title: %s",
|
|
737
|
+
self,
|
|
738
|
+
)
|
|
739
|
+
new_item.title = f"{prev_title} (derived copy)"
|
|
697
740
|
|
|
698
741
|
return new_item
|
|
699
742
|
|
kash/model/params_model.py
CHANGED
|
@@ -294,6 +294,18 @@ COMMON_ACTION_PARAMS: dict[str, Param] = {
|
|
|
294
294
|
type=DocSelection,
|
|
295
295
|
default_value=DocSelection.full,
|
|
296
296
|
),
|
|
297
|
+
"s3_bucket": Param(
|
|
298
|
+
"s3_bucket",
|
|
299
|
+
"The S3 bucket to upload to.",
|
|
300
|
+
type=str,
|
|
301
|
+
default_value=None,
|
|
302
|
+
),
|
|
303
|
+
"s3_prefix": Param(
|
|
304
|
+
"s3_prefix",
|
|
305
|
+
"The S3 prefix to upload to (with or without a trailing slash).",
|
|
306
|
+
type=str,
|
|
307
|
+
default_value=None,
|
|
308
|
+
),
|
|
297
309
|
}
|
|
298
310
|
|
|
299
311
|
# Extra parameters that are available when an action is invoked from the shell.
|
|
@@ -305,6 +317,18 @@ RUNTIME_ACTION_PARAMS: dict[str, Param] = {
|
|
|
305
317
|
"it produces an output item that already exists.",
|
|
306
318
|
type=bool,
|
|
307
319
|
),
|
|
320
|
+
"refetch": Param(
|
|
321
|
+
"refetch",
|
|
322
|
+
"Forcing re-fetching of any content, not using media or content caches.",
|
|
323
|
+
type=bool,
|
|
324
|
+
default_value=False,
|
|
325
|
+
),
|
|
326
|
+
"no_format": Param(
|
|
327
|
+
"no_format",
|
|
328
|
+
"Do not auto-format (normalize) Markdown outputs.",
|
|
329
|
+
type=bool,
|
|
330
|
+
default_value=False,
|
|
331
|
+
),
|
|
308
332
|
}
|
|
309
333
|
|
|
310
334
|
|
|
@@ -7,6 +7,7 @@ from datetime import UTC, datetime
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import TypeVar
|
|
9
9
|
|
|
10
|
+
import rich
|
|
10
11
|
from strif import abbrev_str
|
|
11
12
|
from thefuzz import fuzz
|
|
12
13
|
|
|
@@ -22,7 +23,7 @@ log = get_logger(__name__)
|
|
|
22
23
|
T = TypeVar("T")
|
|
23
24
|
|
|
24
25
|
# Scores less than this can be dropped early.
|
|
25
|
-
MIN_CUTOFF = Score(
|
|
26
|
+
MIN_CUTOFF = Score(70)
|
|
26
27
|
|
|
27
28
|
|
|
28
29
|
def linear_boost(score: Score, min_score: Score) -> Score:
|
|
@@ -171,10 +172,29 @@ def score_phrase(prefix: str, text: str) -> Score:
|
|
|
171
172
|
Could experiment with this more but it's a rough attempt to balance
|
|
172
173
|
full matches and prefix matches.
|
|
173
174
|
"""
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
175
|
+
if len(prefix) > 5:
|
|
176
|
+
return Score(
|
|
177
|
+
0.4 * fuzz.ratio(prefix, text)
|
|
178
|
+
+ 0.3 * fuzz.token_set_ratio(prefix, text)
|
|
179
|
+
+ 0.3 * fuzz.partial_ratio(prefix, text),
|
|
180
|
+
)
|
|
181
|
+
else:
|
|
182
|
+
return Score(0.6 * fuzz.ratio(prefix, text) + 0.4 * fuzz.token_set_ratio(prefix, text))
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def print_all_scores(prefix: str, text: str):
|
|
186
|
+
rich.inspect(
|
|
187
|
+
{
|
|
188
|
+
"ratio": fuzz.ratio(prefix, text),
|
|
189
|
+
"partial_ratio": fuzz.partial_ratio(prefix, text),
|
|
190
|
+
"token_sort_ratio": fuzz.token_sort_ratio(prefix, text),
|
|
191
|
+
"token_set_ratio": fuzz.token_set_ratio(prefix, text),
|
|
192
|
+
"partial_token_set_ratio": fuzz.partial_token_set_ratio(prefix, text),
|
|
193
|
+
"final": score_phrase(prefix, text),
|
|
194
|
+
},
|
|
195
|
+
methods=False,
|
|
196
|
+
docs=False,
|
|
197
|
+
sort=False,
|
|
178
198
|
)
|
|
179
199
|
|
|
180
200
|
|
|
@@ -281,3 +301,15 @@ def score_snippet(query: str, snippet: CommentedCommand) -> Score:
|
|
|
281
301
|
)
|
|
282
302
|
# Bias a little toward command matches.
|
|
283
303
|
return Score(max(command_score, 0.7 * comment_score))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
## Tests
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_score_phrase():
|
|
310
|
+
assert score_phrase("hello world", "hello world") == Score(100)
|
|
311
|
+
assert Score(90) >= score_phrase("hello world", "hello there world") >= Score(80)
|
|
312
|
+
assert Score(70) >= score_phrase("hello world", "hello there there") >= Score(60)
|
|
313
|
+
assert Score(85) >= score_phrase("duf", "df") >= Score(75)
|
|
314
|
+
assert Score(70) >= score_phrase("wik", "awk") >= Score(60)
|
|
315
|
+
assert Score(60) >= score_phrase("wiki", "awk") >= Score(50)
|
kash/shell/output/kerm_codes.py
CHANGED
|
@@ -72,13 +72,12 @@ from html import escape
|
|
|
72
72
|
from typing import Annotated, Literal, Self, TypeAlias
|
|
73
73
|
from urllib.parse import parse_qs, quote, urlencode, urlparse
|
|
74
74
|
|
|
75
|
+
from clideps.terminal.osc_utils import OscStr, osc8_link, osc8_link_codes, osc8_link_rich, osc_code
|
|
75
76
|
from prompt_toolkit.formatted_text import FormattedText
|
|
76
77
|
from pydantic import BaseModel, Field, TypeAdapter, model_validator
|
|
77
78
|
from rich.style import Style
|
|
78
79
|
from rich.text import Text
|
|
79
80
|
|
|
80
|
-
from kash.shell.utils.osc_utils import OscStr, osc8_link, osc8_link_codes, osc8_link_rich, osc_code
|
|
81
|
-
|
|
82
81
|
KC_VERSION = 0
|
|
83
82
|
"""Version of the Kerm codes format. Update when we make breaking changes."""
|
|
84
83
|
|
|
@@ -8,10 +8,7 @@ from flowmark import Wrap, fill_text
|
|
|
8
8
|
from rich.console import Group
|
|
9
9
|
from rich.text import Text
|
|
10
10
|
|
|
11
|
-
from kash.config.text_styles import
|
|
12
|
-
STYLE_HINT,
|
|
13
|
-
format_success_emoji,
|
|
14
|
-
)
|
|
11
|
+
from kash.config.text_styles import COLOR_FAILURE, COLOR_SUCCESS, STYLE_HINT
|
|
15
12
|
from kash.shell.output.kmarkdown import KMarkdown
|
|
16
13
|
|
|
17
14
|
|
|
@@ -84,6 +81,19 @@ def format_paragraphs(*paragraphs: str | Text | Group) -> Group:
|
|
|
84
81
|
return Group(*text)
|
|
85
82
|
|
|
86
83
|
|
|
84
|
+
EMOJI_TRUE = "✔︎"
|
|
85
|
+
|
|
86
|
+
EMOJI_FALSE = "✘"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def success_emoji(value: bool, success_only: bool = False) -> str:
|
|
90
|
+
return EMOJI_TRUE if value else " " if success_only else EMOJI_FALSE
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def format_success_emoji(value: bool, success_only: bool = False) -> Text:
|
|
94
|
+
return Text(success_emoji(value, success_only), style=COLOR_SUCCESS if value else COLOR_FAILURE)
|
|
95
|
+
|
|
96
|
+
|
|
87
97
|
def format_success(message: str | Text) -> Text:
|
|
88
98
|
return Text.assemble(format_success_emoji(True), message)
|
|
89
99
|
|
kash/shell/shell_main.py
CHANGED
|
@@ -16,12 +16,12 @@ import threading
|
|
|
16
16
|
import xonsh.main
|
|
17
17
|
from strif import quote_if_needed
|
|
18
18
|
|
|
19
|
-
from kash.config.setup import
|
|
19
|
+
from kash.config.setup import kash_setup
|
|
20
20
|
from kash.shell.utils.argparse_utils import WrappedColorFormatter
|
|
21
21
|
from kash.shell.version import get_full_version_name, get_version
|
|
22
22
|
from kash.xonsh_custom.custom_shell import install_to_xonshrc, start_shell
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
kash_setup(rich_logging=True) # Set up logging first.
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
__version__ = get_version()
|
|
@@ -46,5 +46,11 @@ def wrap_with_exception_printing(func: Callable[..., R]) -> Callable[[list[str]]
|
|
|
46
46
|
log.error(f"[{COLOR_ERROR}]Command error:[/{COLOR_ERROR}] %s", summarize_traceback(e))
|
|
47
47
|
log.info("Command error details: %s", e, exc_info=True)
|
|
48
48
|
return None
|
|
49
|
+
except Exception as e:
|
|
50
|
+
# Note xonsh can log exceptions but it will be in the xonsh call stack, which is
|
|
51
|
+
# useless for the user. Better to log all unexpected exception call stack
|
|
52
|
+
# here to our logs.
|
|
53
|
+
log.info("Command error details: %s", e, exc_info=True)
|
|
54
|
+
raise
|
|
49
55
|
|
|
50
56
|
return command
|
kash/shell/utils/native_utils.py
CHANGED
|
@@ -11,15 +11,15 @@ import webbrowser
|
|
|
11
11
|
from enum import Enum
|
|
12
12
|
from pathlib import Path
|
|
13
13
|
|
|
14
|
+
from clideps.pkgs.pkg_check import pkg_check
|
|
15
|
+
from clideps.pkgs.platform_checks import Platform, get_platform
|
|
16
|
+
from clideps.terminal.terminal_images import terminal_show_image
|
|
14
17
|
from flowmark import Wrap
|
|
15
18
|
from funlog import log_calls
|
|
16
19
|
|
|
17
20
|
from kash.config.logger import get_logger
|
|
18
|
-
from kash.config.text_styles import BAT_STYLE, BAT_THEME, COLOR_ERROR
|
|
19
|
-
from kash.shell.clideps.pkg_deps import Pkg, pkg_check
|
|
20
|
-
from kash.shell.clideps.platforms import PLATFORM, Platform
|
|
21
|
+
from kash.config.text_styles import BAT_STYLE, BAT_STYLE_PLAIN, BAT_THEME, COLOR_ERROR
|
|
21
22
|
from kash.shell.output.shell_output import cprint
|
|
22
|
-
from kash.shell.utils.terminal_images import terminal_show_image
|
|
23
23
|
from kash.utils.common.format_utils import fmt_loc
|
|
24
24
|
from kash.utils.common.url import as_file_url, is_file_url, is_url
|
|
25
25
|
from kash.utils.errors import FileNotFound, SetupError
|
|
@@ -49,11 +49,11 @@ def file_size_check(
|
|
|
49
49
|
def native_open(filename: str | Path):
|
|
50
50
|
filename = str(filename)
|
|
51
51
|
log.message("Opening file: %s", filename)
|
|
52
|
-
if
|
|
52
|
+
if get_platform() == Platform.Darwin:
|
|
53
53
|
subprocess.run(["open", filename])
|
|
54
|
-
elif
|
|
54
|
+
elif get_platform() == Platform.Linux:
|
|
55
55
|
subprocess.run(["xdg-open", filename])
|
|
56
|
-
elif
|
|
56
|
+
elif get_platform() == Platform.Windows:
|
|
57
57
|
subprocess.run(["start", shlex.quote(filename)], shell=True)
|
|
58
58
|
else:
|
|
59
59
|
raise NotImplementedError("Unsupported platform")
|
|
@@ -110,12 +110,14 @@ def _detect_view_mode(file_or_url: str) -> ViewMode:
|
|
|
110
110
|
def view_file_native(
|
|
111
111
|
file_or_url: str | Path,
|
|
112
112
|
view_mode: ViewMode = ViewMode.auto,
|
|
113
|
+
plain: bool = False,
|
|
113
114
|
):
|
|
114
115
|
"""
|
|
115
116
|
Open a file or URL in the console or a native app. If `view_mode` is auto,
|
|
116
117
|
automatically determine whether to use console, web browser, or the user's
|
|
117
118
|
preferred native application. For images, also tries terminal-based image
|
|
118
|
-
display.
|
|
119
|
+
display. The `--plain` flag will disable line numbers, grid, etc. in `bat`
|
|
120
|
+
and force `ViewMode.console`.
|
|
119
121
|
"""
|
|
120
122
|
file_or_url = str(file_or_url)
|
|
121
123
|
path = None
|
|
@@ -124,6 +126,9 @@ def view_file_native(
|
|
|
124
126
|
if not path.exists():
|
|
125
127
|
raise FileNotFound(fmt_loc(path))
|
|
126
128
|
|
|
129
|
+
if plain:
|
|
130
|
+
view_mode = ViewMode.console
|
|
131
|
+
|
|
127
132
|
if view_mode == ViewMode.auto:
|
|
128
133
|
view_mode = _detect_view_mode(file_or_url)
|
|
129
134
|
|
|
@@ -133,7 +138,7 @@ def view_file_native(
|
|
|
133
138
|
webbrowser.open(url)
|
|
134
139
|
elif view_mode == ViewMode.console and path:
|
|
135
140
|
file_size, min_lines = file_size_check(path)
|
|
136
|
-
view_file_console(path, use_pager=min_lines > 40 or file_size > 20 * 1024)
|
|
141
|
+
view_file_console(path, use_pager=min_lines > 40 or file_size > 20 * 1024, plain=plain)
|
|
137
142
|
elif view_mode == ViewMode.terminal_image and path:
|
|
138
143
|
try:
|
|
139
144
|
terminal_show_image(path)
|
|
@@ -187,11 +192,11 @@ def tail_file(
|
|
|
187
192
|
if follow:
|
|
188
193
|
max_lines = follow_max_lines
|
|
189
194
|
|
|
190
|
-
pkg_check().require(
|
|
191
|
-
pkg_check().warn_if_missing(
|
|
195
|
+
pkg_check().require("tail")
|
|
196
|
+
pkg_check().warn_if_missing("bat")
|
|
192
197
|
|
|
193
198
|
if follow:
|
|
194
|
-
if pkg_check().
|
|
199
|
+
if pkg_check().is_found("bat"):
|
|
195
200
|
# Follow the file in real-time.
|
|
196
201
|
command = (
|
|
197
202
|
f"tail -{max_lines} -f {all_paths_str} | "
|
|
@@ -202,8 +207,8 @@ def tail_file(
|
|
|
202
207
|
command = f"tail -f {all_paths_str}"
|
|
203
208
|
cprint("Following file: `%s`", command, text_wrap=Wrap.NONE)
|
|
204
209
|
else:
|
|
205
|
-
pkg_check().require(
|
|
206
|
-
if pkg_check().
|
|
210
|
+
pkg_check().require("less")
|
|
211
|
+
if pkg_check().is_found("bat"):
|
|
207
212
|
command = (
|
|
208
213
|
f"tail -{max_lines} {all_paths_str} | "
|
|
209
214
|
f"bat --paging=never --color=always --style=plain --theme={BAT_THEME} -l log | "
|
|
@@ -216,7 +221,7 @@ def tail_file(
|
|
|
216
221
|
subprocess.run(command, shell=True, check=True)
|
|
217
222
|
|
|
218
223
|
|
|
219
|
-
def view_file_console(filename: str | Path, use_pager: bool = True):
|
|
224
|
+
def view_file_console(filename: str | Path, use_pager: bool = True, plain: bool = False):
|
|
220
225
|
"""
|
|
221
226
|
Displays a file in the console with pagination and syntax highlighting.
|
|
222
227
|
"""
|
|
@@ -226,18 +231,19 @@ def view_file_console(filename: str | Path, use_pager: bool = True):
|
|
|
226
231
|
# TODO: Visualize YAML frontmatter with different syntax/style than Markdown content.
|
|
227
232
|
|
|
228
233
|
is_text = file_format_info(filename).is_text
|
|
234
|
+
bat_style = BAT_STYLE_PLAIN if plain else BAT_STYLE
|
|
229
235
|
if is_text:
|
|
230
|
-
pkg_check().require(
|
|
231
|
-
if pkg_check().
|
|
236
|
+
pkg_check().require("less")
|
|
237
|
+
if pkg_check().is_found("bat"):
|
|
232
238
|
pager_str = "--pager=always --pager=less " if use_pager else ""
|
|
233
|
-
command = f"bat {pager_str}--color=always --style={
|
|
239
|
+
command = f"bat {pager_str}--color=always --style={bat_style} --theme={BAT_THEME} {quoted_filename}"
|
|
234
240
|
else:
|
|
235
|
-
pkg_check().require(
|
|
241
|
+
pkg_check().require("pygmentize")
|
|
236
242
|
command = f"pygmentize -g {quoted_filename}"
|
|
237
243
|
if use_pager:
|
|
238
244
|
command = f"{command} | less -R"
|
|
239
245
|
else:
|
|
240
|
-
pkg_check().require(
|
|
246
|
+
pkg_check().require("hexyl")
|
|
241
247
|
command = f"hexyl {quoted_filename}"
|
|
242
248
|
if use_pager:
|
|
243
249
|
command = f"{command} | less -R"
|
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
from collections.abc import Callable
|
|
2
2
|
from math import ceil
|
|
3
3
|
|
|
4
|
-
from chopdiff.docs import
|
|
4
|
+
from chopdiff.docs import (
|
|
5
|
+
DIFF_FILTER_NONE,
|
|
6
|
+
DiffFilter,
|
|
7
|
+
Paragraph,
|
|
8
|
+
TextDoc,
|
|
9
|
+
TextUnit,
|
|
10
|
+
diff_docs,
|
|
11
|
+
join_wordtoks,
|
|
12
|
+
)
|
|
5
13
|
from chopdiff.transforms import (
|
|
6
14
|
WindowSettings,
|
|
7
|
-
accept_all,
|
|
8
15
|
remove_window_br,
|
|
9
16
|
sliding_para_window,
|
|
10
17
|
sliding_window_transform,
|
|
@@ -31,7 +38,7 @@ def filtered_transform(
|
|
|
31
38
|
doc: TextDoc,
|
|
32
39
|
transform_func: TextDocTransform,
|
|
33
40
|
windowing: WindowSettings | None,
|
|
34
|
-
diff_filter: DiffFilter =
|
|
41
|
+
diff_filter: DiffFilter | None = None,
|
|
35
42
|
) -> TextDoc:
|
|
36
43
|
"""
|
|
37
44
|
Apply a transform with sliding window across the input doc, enforcing the changes it's
|
|
@@ -39,7 +46,7 @@ def filtered_transform(
|
|
|
39
46
|
|
|
40
47
|
If windowing is None, apply the transform to the entire document at once.
|
|
41
48
|
"""
|
|
42
|
-
has_filter = diff_filter !=
|
|
49
|
+
has_filter = bool(diff_filter and diff_filter != DIFF_FILTER_NONE)
|
|
43
50
|
|
|
44
51
|
if not windowing or not windowing.size:
|
|
45
52
|
transformed_doc = transform_func(doc)
|
|
@@ -52,6 +59,7 @@ def filtered_transform(
|
|
|
52
59
|
transformed_doc = transform_func(input_doc)
|
|
53
60
|
|
|
54
61
|
if has_filter:
|
|
62
|
+
assert diff_filter
|
|
55
63
|
# Check the transform did what it should have.
|
|
56
64
|
diff = diff_docs(input_doc, transformed_doc)
|
|
57
65
|
accepted_diff, rejected_diff = diff.filter(diff_filter)
|
|
@@ -21,7 +21,11 @@ def normalize_formatting_ansi(text: str, format: Format | None, width=DEFAULT_WR
|
|
|
21
21
|
text, width=width, word_splitter=simple_word_splitter, len_fn=ansi_cell_len
|
|
22
22
|
)
|
|
23
23
|
elif format == Format.markdown or format == Format.md_html:
|
|
24
|
-
return fill_markdown(
|
|
24
|
+
return fill_markdown(
|
|
25
|
+
text,
|
|
26
|
+
line_wrapper=line_wrap_by_sentence(len_fn=ansi_cell_len),
|
|
27
|
+
cleanups=True, # Safe cleanups like unbolding section headers.
|
|
28
|
+
)
|
|
25
29
|
elif format == Format.html:
|
|
26
30
|
# We don't currently auto-format HTML as we sometimes use HTML with specifically chosen line breaks.
|
|
27
31
|
return text
|
|
@@ -52,7 +56,7 @@ def normalize_text_file(
|
|
|
52
56
|
|
|
53
57
|
|
|
54
58
|
def test_osc8_link():
|
|
55
|
-
from
|
|
59
|
+
from clideps.terminal.osc_utils import osc8_link
|
|
56
60
|
|
|
57
61
|
link = osc8_link("https://example.com/" + "x" * 50, "Example")
|
|
58
62
|
assert ansi_cell_len(link) == 7
|