termflow-md 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- termflow/__init__.py +58 -0
- termflow/__main__.py +6 -0
- termflow/ansi/__init__.py +103 -0
- termflow/ansi/codes.py +92 -0
- termflow/ansi/color.py +160 -0
- termflow/ansi/style.py +51 -0
- termflow/ansi/utils.py +352 -0
- termflow/cli.py +310 -0
- termflow/config/__init__.py +23 -0
- termflow/config/config.py +251 -0
- termflow/core/__init__.py +37 -0
- termflow/core/enums.py +151 -0
- termflow/core/state.py +549 -0
- termflow/parser/__init__.py +123 -0
- termflow/parser/entities.py +66 -0
- termflow/parser/events.py +352 -0
- termflow/parser/inline.py +303 -0
- termflow/parser/parser.py +662 -0
- termflow/render/__init__.py +134 -0
- termflow/render/code.py +190 -0
- termflow/render/heading.py +123 -0
- termflow/render/list.py +186 -0
- termflow/render/renderer.py +602 -0
- termflow/render/style.py +145 -0
- termflow/render/table.py +236 -0
- termflow/render/text.py +203 -0
- termflow/syntax/__init__.py +32 -0
- termflow/syntax/highlighter.py +506 -0
- termflow_md-0.1.0.dist-info/METADATA +330 -0
- termflow_md-0.1.0.dist-info/RECORD +33 -0
- termflow_md-0.1.0.dist-info/WHEEL +4 -0
- termflow_md-0.1.0.dist-info/entry_points.txt +2 -0
- termflow_md-0.1.0.dist-info/licenses/LICENSE +21 -0
termflow/__init__.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""termflow - A streaming markdown renderer for modern terminals.
|
|
2
|
+
|
|
3
|
+
A Python port of streamdown-rs, providing beautiful markdown rendering
|
|
4
|
+
directly in your terminal with streaming support, syntax highlighting,
|
|
5
|
+
and rich formatting.
|
|
6
|
+
|
|
7
|
+
Basic usage:
|
|
8
|
+
>>> from termflow import render_markdown
|
|
9
|
+
>>> render_markdown("# Hello World!")
|
|
10
|
+
|
|
11
|
+
Streaming usage:
|
|
12
|
+
>>> from termflow import Parser, Renderer
|
|
13
|
+
>>> parser = Parser()
|
|
14
|
+
>>> renderer = Renderer(width=80)
|
|
15
|
+
>>>
|
|
16
|
+
>>> for line in markdown_lines:
|
|
17
|
+
... events = parser.parse_line(line)
|
|
18
|
+
... renderer.render_all(events)
|
|
19
|
+
>>>
|
|
20
|
+
>>> renderer.render_all(parser.finalize())
|
|
21
|
+
|
|
22
|
+
With configuration:
|
|
23
|
+
>>> from termflow import Config, Renderer, RenderStyle
|
|
24
|
+
>>> config = Config.load()
|
|
25
|
+
>>> style = RenderStyle.dracula()
|
|
26
|
+
>>> renderer = Renderer(style=style, features=config.features)
|
|
27
|
+
|
|
28
|
+
CLI usage:
|
|
29
|
+
$ cat README.md | tf
|
|
30
|
+
$ tf document.md
|
|
31
|
+
$ tf --style dracula README.md
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
__version__ = "0.1.0"
|
|
35
|
+
|
|
36
|
+
from termflow.config import Config
|
|
37
|
+
from termflow.parser import Parser
|
|
38
|
+
from termflow.parser.events import ParseEvent
|
|
39
|
+
from termflow.render import Renderer, RenderFeatures, RenderStyle, render_markdown
|
|
40
|
+
from termflow.syntax import Highlighter, highlight_code
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
# Core classes
|
|
44
|
+
"Config",
|
|
45
|
+
"Highlighter",
|
|
46
|
+
# Events
|
|
47
|
+
"ParseEvent",
|
|
48
|
+
"Parser",
|
|
49
|
+
# Style
|
|
50
|
+
"RenderFeatures",
|
|
51
|
+
"RenderStyle",
|
|
52
|
+
"Renderer",
|
|
53
|
+
# Version
|
|
54
|
+
"__version__",
|
|
55
|
+
# Convenience functions
|
|
56
|
+
"highlight_code",
|
|
57
|
+
"render_markdown",
|
|
58
|
+
]
|
termflow/__main__.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""ANSI module - terminal escape codes and styling.
|
|
2
|
+
|
|
3
|
+
This module provides comprehensive ANSI escape code support for terminal
|
|
4
|
+
rendering, including:
|
|
5
|
+
|
|
6
|
+
- Raw escape code constants (codes.py)
|
|
7
|
+
- Color conversion utilities (color.py)
|
|
8
|
+
- Style pairs for easy toggling (style.py)
|
|
9
|
+
- Text processing utilities (utils.py)
|
|
10
|
+
|
|
11
|
+
Example:
|
|
12
|
+
>>> from termflow.ansi import BOLD, fg_color, visible_length
|
|
13
|
+
>>> styled = f"{BOLD[0]}{fg_color('#FF5500')}Orange Bold{BOLD[1]}"
|
|
14
|
+
>>> print(visible_length(styled)) # Returns 11, not counting ANSI codes
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from termflow.ansi.codes import (
|
|
18
|
+
BGRESET,
|
|
19
|
+
BOLD_OFF,
|
|
20
|
+
BOLD_ON,
|
|
21
|
+
DIM_OFF,
|
|
22
|
+
DIM_ON,
|
|
23
|
+
ITALIC_OFF,
|
|
24
|
+
ITALIC_ON,
|
|
25
|
+
RESET,
|
|
26
|
+
STRIKEOUT_OFF,
|
|
27
|
+
STRIKEOUT_ON,
|
|
28
|
+
SUPERSCRIPTS,
|
|
29
|
+
UNDERLINE_OFF,
|
|
30
|
+
UNDERLINE_ON,
|
|
31
|
+
digit_to_superscript,
|
|
32
|
+
number_to_superscript,
|
|
33
|
+
)
|
|
34
|
+
from termflow.ansi.color import (
|
|
35
|
+
bg_color,
|
|
36
|
+
fg_color,
|
|
37
|
+
hex2rgb,
|
|
38
|
+
hsv_to_rgb,
|
|
39
|
+
rgb2hex,
|
|
40
|
+
)
|
|
41
|
+
from termflow.ansi.style import (
|
|
42
|
+
BOLD,
|
|
43
|
+
DIM,
|
|
44
|
+
ITALIC,
|
|
45
|
+
LINK,
|
|
46
|
+
STRIKEOUT,
|
|
47
|
+
UNDERLINE,
|
|
48
|
+
make_link,
|
|
49
|
+
)
|
|
50
|
+
from termflow.ansi.utils import (
|
|
51
|
+
ANSI_CSI_RE,
|
|
52
|
+
ANSI_ESCAPE_RE,
|
|
53
|
+
ANSI_SGR_RE,
|
|
54
|
+
extract_ansi_codes,
|
|
55
|
+
is_ansi_code,
|
|
56
|
+
parse_sgr_params,
|
|
57
|
+
split_ansi,
|
|
58
|
+
truncate_ansi,
|
|
59
|
+
visible,
|
|
60
|
+
visible_length,
|
|
61
|
+
wrap_ansi,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
__all__ = [
|
|
65
|
+
"ANSI_CSI_RE",
|
|
66
|
+
"ANSI_ESCAPE_RE",
|
|
67
|
+
"ANSI_SGR_RE",
|
|
68
|
+
"BGRESET",
|
|
69
|
+
"BOLD",
|
|
70
|
+
"BOLD_OFF",
|
|
71
|
+
"BOLD_ON",
|
|
72
|
+
"DIM",
|
|
73
|
+
"DIM_OFF",
|
|
74
|
+
"DIM_ON",
|
|
75
|
+
"ITALIC",
|
|
76
|
+
"ITALIC_OFF",
|
|
77
|
+
"ITALIC_ON",
|
|
78
|
+
"LINK",
|
|
79
|
+
"RESET",
|
|
80
|
+
"STRIKEOUT",
|
|
81
|
+
"STRIKEOUT_OFF",
|
|
82
|
+
"STRIKEOUT_ON",
|
|
83
|
+
"SUPERSCRIPTS",
|
|
84
|
+
"UNDERLINE",
|
|
85
|
+
"UNDERLINE_OFF",
|
|
86
|
+
"UNDERLINE_ON",
|
|
87
|
+
"bg_color",
|
|
88
|
+
"digit_to_superscript",
|
|
89
|
+
"extract_ansi_codes",
|
|
90
|
+
"fg_color",
|
|
91
|
+
"hex2rgb",
|
|
92
|
+
"hsv_to_rgb",
|
|
93
|
+
"is_ansi_code",
|
|
94
|
+
"make_link",
|
|
95
|
+
"number_to_superscript",
|
|
96
|
+
"parse_sgr_params",
|
|
97
|
+
"rgb2hex",
|
|
98
|
+
"split_ansi",
|
|
99
|
+
"truncate_ansi",
|
|
100
|
+
"visible",
|
|
101
|
+
"visible_length",
|
|
102
|
+
"wrap_ansi",
|
|
103
|
+
]
|
termflow/ansi/codes.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"""ANSI escape code constants and superscript utilities.
|
|
2
|
+
|
|
3
|
+
This module provides all the raw ANSI escape sequences needed for terminal
|
|
4
|
+
text styling, along with utilities for converting digits to Unicode superscripts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# =============================================================================
|
|
8
|
+
# Reset
|
|
9
|
+
# =============================================================================
|
|
10
|
+
RESET = "\x1b[0m"
|
|
11
|
+
|
|
12
|
+
# =============================================================================
|
|
13
|
+
# Text Styles - On/Off pairs
|
|
14
|
+
# =============================================================================
|
|
15
|
+
BOLD_ON = "\x1b[1m"
|
|
16
|
+
BOLD_OFF = "\x1b[22m" # Also turns off DIM
|
|
17
|
+
|
|
18
|
+
DIM_ON = "\x1b[2m"
|
|
19
|
+
DIM_OFF = "\x1b[22m" # Also turns off BOLD
|
|
20
|
+
|
|
21
|
+
ITALIC_ON = "\x1b[3m"
|
|
22
|
+
ITALIC_OFF = "\x1b[23m"
|
|
23
|
+
|
|
24
|
+
UNDERLINE_ON = "\x1b[4m"
|
|
25
|
+
UNDERLINE_OFF = "\x1b[24m"
|
|
26
|
+
|
|
27
|
+
STRIKEOUT_ON = "\x1b[9m"
|
|
28
|
+
STRIKEOUT_OFF = "\x1b[29m"
|
|
29
|
+
|
|
30
|
+
# =============================================================================
|
|
31
|
+
# Background
|
|
32
|
+
# =============================================================================
|
|
33
|
+
BGRESET = "\x1b[49m"
|
|
34
|
+
|
|
35
|
+
# =============================================================================
|
|
36
|
+
# Superscript Unicode Characters
|
|
37
|
+
# =============================================================================
|
|
38
|
+
SUPERSCRIPTS = "⁰¹²³⁴⁵⁶⁷⁸⁹"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def digit_to_superscript(digit: int) -> str:
|
|
42
|
+
"""Convert a digit (0-9) to its superscript Unicode character.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
digit: An integer from 0 to 9.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
The corresponding superscript Unicode character.
|
|
49
|
+
|
|
50
|
+
Raises:
|
|
51
|
+
ValueError: If digit is not in range 0-9.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> digit_to_superscript(2)
|
|
55
|
+
'²'
|
|
56
|
+
>>> digit_to_superscript(0)
|
|
57
|
+
'⁰'
|
|
58
|
+
"""
|
|
59
|
+
if not 0 <= digit <= 9:
|
|
60
|
+
raise ValueError(f"Digit must be 0-9, got {digit}")
|
|
61
|
+
return SUPERSCRIPTS[digit]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def number_to_superscript(num: int) -> str:
|
|
65
|
+
"""Convert an integer to its superscript string representation.
|
|
66
|
+
|
|
67
|
+
Handles negative numbers by prefixing with superscript minus (⁻).
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
num: Any integer.
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
The superscript representation of the number.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
>>> number_to_superscript(42)
|
|
77
|
+
'⁴²'
|
|
78
|
+
>>> number_to_superscript(-5)
|
|
79
|
+
'⁻⁵'
|
|
80
|
+
>>> number_to_superscript(0)
|
|
81
|
+
'⁰'
|
|
82
|
+
"""
|
|
83
|
+
if num < 0:
|
|
84
|
+
return "⁻" + number_to_superscript(-num)
|
|
85
|
+
if num == 0:
|
|
86
|
+
return SUPERSCRIPTS[0]
|
|
87
|
+
|
|
88
|
+
result = []
|
|
89
|
+
while num > 0:
|
|
90
|
+
result.append(SUPERSCRIPTS[num % 10])
|
|
91
|
+
num //= 10
|
|
92
|
+
return "".join(reversed(result))
|
termflow/ansi/color.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""HSV/RGB color utilities for ANSI terminal colors.
|
|
2
|
+
|
|
3
|
+
This module provides color conversion functions and ANSI escape sequence
|
|
4
|
+
generators for 24-bit (truecolor) terminal support.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
# Regex for hex color validation
|
|
10
|
+
_HEX_COLOR_RE = re.compile(r"^#?([0-9a-fA-F]{6})$")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def hex2rgb(hex_color: str) -> tuple[int, int, int] | None:
|
|
14
|
+
"""Convert hex color (#RRGGBB or RRGGBB) to RGB tuple.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
hex_color: Hex color string, with or without leading '#'.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
Tuple of (R, G, B) values (0-255 each), or None if invalid.
|
|
21
|
+
|
|
22
|
+
Example:
|
|
23
|
+
>>> hex2rgb("#FF5500")
|
|
24
|
+
(255, 85, 0)
|
|
25
|
+
>>> hex2rgb("00FF00")
|
|
26
|
+
(0, 255, 0)
|
|
27
|
+
>>> hex2rgb("invalid")
|
|
28
|
+
None
|
|
29
|
+
"""
|
|
30
|
+
match = _HEX_COLOR_RE.match(hex_color)
|
|
31
|
+
if not match:
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
hex_str = match.group(1)
|
|
35
|
+
return (
|
|
36
|
+
int(hex_str[0:2], 16),
|
|
37
|
+
int(hex_str[2:4], 16),
|
|
38
|
+
int(hex_str[4:6], 16),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def rgb2hex(r: int, g: int, b: int) -> str:
|
|
43
|
+
"""Convert RGB values to hex string (#RRGGBB).
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
r: Red component (0-255).
|
|
47
|
+
g: Green component (0-255).
|
|
48
|
+
b: Blue component (0-255).
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Hex color string with leading '#'.
|
|
52
|
+
|
|
53
|
+
Example:
|
|
54
|
+
>>> rgb2hex(255, 85, 0)
|
|
55
|
+
'#FF5500'
|
|
56
|
+
>>> rgb2hex(0, 0, 0)
|
|
57
|
+
'#000000'
|
|
58
|
+
"""
|
|
59
|
+
# Clamp values to valid range
|
|
60
|
+
r = max(0, min(255, r))
|
|
61
|
+
g = max(0, min(255, g))
|
|
62
|
+
b = max(0, min(255, b))
|
|
63
|
+
return f"#{r:02X}{g:02X}{b:02X}"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def hsv_to_rgb(h: float, s: float, v: float) -> tuple[int, int, int]:
|
|
67
|
+
"""Convert HSV color to RGB.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
h: Hue (0.0-1.0).
|
|
71
|
+
s: Saturation (0.0-1.0).
|
|
72
|
+
v: Value/brightness (0.0-1.0).
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Tuple of (R, G, B) values (0-255 each).
|
|
76
|
+
|
|
77
|
+
Example:
|
|
78
|
+
>>> hsv_to_rgb(0.0, 1.0, 1.0) # Red
|
|
79
|
+
(255, 0, 0)
|
|
80
|
+
>>> hsv_to_rgb(0.333, 1.0, 1.0) # Green-ish
|
|
81
|
+
(0, 255, 2)
|
|
82
|
+
>>> hsv_to_rgb(0.0, 0.0, 1.0) # White
|
|
83
|
+
(255, 255, 255)
|
|
84
|
+
"""
|
|
85
|
+
if s == 0.0:
|
|
86
|
+
# Achromatic (grey)
|
|
87
|
+
val = int(v * 255)
|
|
88
|
+
return (val, val, val)
|
|
89
|
+
|
|
90
|
+
h = h % 1.0 # Wrap hue to 0-1 range
|
|
91
|
+
h *= 6.0
|
|
92
|
+
i = int(h)
|
|
93
|
+
f = h - i
|
|
94
|
+
|
|
95
|
+
p = v * (1.0 - s)
|
|
96
|
+
q = v * (1.0 - s * f)
|
|
97
|
+
t = v * (1.0 - s * (1.0 - f))
|
|
98
|
+
|
|
99
|
+
if i == 0:
|
|
100
|
+
r, g, b = v, t, p
|
|
101
|
+
elif i == 1:
|
|
102
|
+
r, g, b = q, v, p
|
|
103
|
+
elif i == 2:
|
|
104
|
+
r, g, b = p, v, t
|
|
105
|
+
elif i == 3:
|
|
106
|
+
r, g, b = p, q, v
|
|
107
|
+
elif i == 4:
|
|
108
|
+
r, g, b = t, p, v
|
|
109
|
+
else:
|
|
110
|
+
r, g, b = v, p, q
|
|
111
|
+
|
|
112
|
+
return (int(r * 255), int(g * 255), int(b * 255))
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def fg_color(hex_color: str) -> str:
|
|
116
|
+
"""Generate foreground ANSI escape sequence from hex color.
|
|
117
|
+
|
|
118
|
+
Uses 24-bit truecolor ANSI escape codes (\x1b[38;2;R;G;Bm).
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
hex_color: Hex color string (#RRGGBB or RRGGBB).
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
ANSI escape sequence for foreground color, or empty string if invalid.
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
>>> fg_color("#FF5500")
|
|
128
|
+
'\\x1b[38;2;255;85;0m'
|
|
129
|
+
>>> fg_color("invalid")
|
|
130
|
+
''
|
|
131
|
+
"""
|
|
132
|
+
rgb = hex2rgb(hex_color)
|
|
133
|
+
if rgb is None:
|
|
134
|
+
return ""
|
|
135
|
+
r, g, b = rgb
|
|
136
|
+
return f"\x1b[38;2;{r};{g};{b}m"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def bg_color(hex_color: str) -> str:
|
|
140
|
+
"""Generate background ANSI escape sequence from hex color.
|
|
141
|
+
|
|
142
|
+
Uses 24-bit truecolor ANSI escape codes (\x1b[48;2;R;G;Bm).
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
hex_color: Hex color string (#RRGGBB or RRGGBB).
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
ANSI escape sequence for background color, or empty string if invalid.
|
|
149
|
+
|
|
150
|
+
Example:
|
|
151
|
+
>>> bg_color("#FF5500")
|
|
152
|
+
'\\x1b[48;2;255;85;0m'
|
|
153
|
+
>>> bg_color("invalid")
|
|
154
|
+
''
|
|
155
|
+
"""
|
|
156
|
+
rgb = hex2rgb(hex_color)
|
|
157
|
+
if rgb is None:
|
|
158
|
+
return ""
|
|
159
|
+
r, g, b = rgb
|
|
160
|
+
return f"\x1b[48;2;{r};{g};{b}m"
|
termflow/ansi/style.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Style pairs (on, off) tuples for easy toggling.
|
|
2
|
+
|
|
3
|
+
These tuples make it easy to wrap text with styles:
|
|
4
|
+
styled_text = f"{BOLD[0]}bold text{BOLD[1]}"
|
|
5
|
+
|
|
6
|
+
Each tuple is (on_code, off_code) for symmetric style application.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# =============================================================================
|
|
10
|
+
# Text Style Pairs: (on, off)
|
|
11
|
+
# =============================================================================
|
|
12
|
+
|
|
13
|
+
#: Bold text style - Note: BOLD_OFF (22m) also turns off DIM
|
|
14
|
+
BOLD: tuple[str, str] = ("\x1b[1m", "\x1b[22m")
|
|
15
|
+
|
|
16
|
+
#: Dim/faint text style - Note: DIM_OFF (22m) also turns off BOLD
|
|
17
|
+
DIM: tuple[str, str] = ("\x1b[2m", "\x1b[22m")
|
|
18
|
+
|
|
19
|
+
#: Italic text style
|
|
20
|
+
ITALIC: tuple[str, str] = ("\x1b[3m", "\x1b[23m")
|
|
21
|
+
|
|
22
|
+
#: Underline text style
|
|
23
|
+
UNDERLINE: tuple[str, str] = ("\x1b[4m", "\x1b[24m")
|
|
24
|
+
|
|
25
|
+
#: Strikethrough/strikeout text style
|
|
26
|
+
STRIKEOUT: tuple[str, str] = ("\x1b[9m", "\x1b[29m")
|
|
27
|
+
|
|
28
|
+
# =============================================================================
|
|
29
|
+
# OSC 8 Hyperlink
|
|
30
|
+
# =============================================================================
|
|
31
|
+
|
|
32
|
+
#: OSC 8 hyperlink - Usage: f"{LINK[0]}{url}\x1b\\{text}{LINK[1]}"
|
|
33
|
+
#: The URL goes after LINK[0], then ST (\x1b\\), then visible text, then LINK[1]
|
|
34
|
+
LINK: tuple[str, str] = ("\x1b]8;;", "\x1b]8;;\x1b\\")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def make_link(url: str, text: str) -> str:
|
|
38
|
+
"""Create an OSC 8 hyperlink.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
url: The URL to link to.
|
|
42
|
+
text: The visible text to display.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
ANSI-escaped hyperlink string.
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
>>> link = make_link("https://example.com", "Click here")
|
|
49
|
+
>>> # Renders as clickable "Click here" in supported terminals
|
|
50
|
+
"""
|
|
51
|
+
return f"{LINK[0]}{url}\x1b\\{text}{LINK[1]}"
|