streamdown 0.34.0__py3-none-any.whl → 0.35.1__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.
- streamdown/sd.py +52 -17
- {streamdown-0.34.0.dist-info → streamdown-0.35.1.dist-info}/METADATA +6 -2
- {streamdown-0.34.0.dist-info → streamdown-0.35.1.dist-info}/RECORD +6 -6
- {streamdown-0.34.0.dist-info → streamdown-0.35.1.dist-info}/WHEEL +1 -1
- {streamdown-0.34.0.dist-info → streamdown-0.35.1.dist-info}/entry_points.txt +0 -0
- {streamdown-0.34.0.dist-info → streamdown-0.35.1.dist-info}/licenses/LICENSE.MIT +0 -0
streamdown/sd.py
CHANGED
|
@@ -57,6 +57,8 @@ Clipboard = true
|
|
|
57
57
|
Logging = false
|
|
58
58
|
Timeout = 0.1
|
|
59
59
|
Savebrace = true
|
|
60
|
+
Images = true
|
|
61
|
+
Links = true
|
|
60
62
|
|
|
61
63
|
[style]
|
|
62
64
|
Margin = 2
|
|
@@ -115,6 +117,7 @@ visible = lambda x: re.sub(ANSIESCAPE, "", x)
|
|
|
115
117
|
# many characters have different widths
|
|
116
118
|
visible_length = lambda x: sum(wcwidth(c) for c in visible(x))
|
|
117
119
|
extract_ansi_codes = lambda text: re.findall(ESCAPE, text)
|
|
120
|
+
strip_ansi = lambda line: re.sub(ANSIESCAPE, '', line)
|
|
118
121
|
remove_ansi = lambda line, codeList: reduce(lambda line, code: line.replace(code, ''), codeList, line)
|
|
119
122
|
split_up = lambda line: re.findall(r'(\x1b[^m]*m|[^\x1b]*)', line)
|
|
120
123
|
|
|
@@ -127,7 +130,10 @@ def gettmpdir():
|
|
|
127
130
|
else:
|
|
128
131
|
tmp_dir = tmp_dir_all
|
|
129
132
|
|
|
133
|
+
prev_mask = os.umask(0)
|
|
134
|
+
# This is shared among all users
|
|
130
135
|
os.makedirs(tmp_dir, exist_ok=True)
|
|
136
|
+
os.umask(prev_mask)
|
|
131
137
|
return tmp_dir
|
|
132
138
|
|
|
133
139
|
def debug_write(text):
|
|
@@ -136,6 +142,10 @@ def debug_write(text):
|
|
|
136
142
|
state.Logging = tempfile.NamedTemporaryFile(dir=gettmpdir(), prefix="dbg", delete=False, mode="wb")
|
|
137
143
|
state.Logging.write(text)
|
|
138
144
|
|
|
145
|
+
def sub_extract(line, upto):
|
|
146
|
+
# this takes the line up to the upto string and then returns the ansi codes
|
|
147
|
+
return ''.join(extract_ansi_codes(line[:line.find(upto)]))
|
|
148
|
+
|
|
139
149
|
def savebrace():
|
|
140
150
|
if state.Savebrace and state.code_buffer_raw and os.name != 'nt':
|
|
141
151
|
path = os.path.join(gettmpdir(), 'savebrace')
|
|
@@ -165,6 +175,7 @@ class ParseState:
|
|
|
165
175
|
self.is_pty = False
|
|
166
176
|
self.is_exec = False
|
|
167
177
|
self.maybe_prompt = False
|
|
178
|
+
self.prompt_regex = None
|
|
168
179
|
self.emit_flag = None
|
|
169
180
|
self.scrape = None
|
|
170
181
|
self.scrape_ix = 0
|
|
@@ -204,6 +215,7 @@ class ParseState:
|
|
|
204
215
|
self.in_underline = False
|
|
205
216
|
self.in_strikeout = False
|
|
206
217
|
self.block_depth = 0
|
|
218
|
+
self.block_type = None
|
|
207
219
|
|
|
208
220
|
self.exec_sub = None
|
|
209
221
|
self.exec_master = None
|
|
@@ -442,7 +454,7 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
|
|
|
442
454
|
# that we have closed our hyperlink OSC
|
|
443
455
|
if LINK[0] in line_content:
|
|
444
456
|
line_content += LINK[1]
|
|
445
|
-
lines.append(line_content + resetter + state.bg + ' ' * margin)
|
|
457
|
+
lines.append(line_content + resetter + sub_extract(line_content, LINK[0]) + state.bg + ' ' * margin)
|
|
446
458
|
|
|
447
459
|
current_line = (" " * indent) + "".join(current_style) + word
|
|
448
460
|
|
|
@@ -492,12 +504,19 @@ def line_format(line):
|
|
|
492
504
|
|
|
493
505
|
# Apply OSC 8 hyperlink formatting after other formatting
|
|
494
506
|
def process_links(match):
|
|
507
|
+
#import pdb
|
|
508
|
+
#pdb.set_trace()
|
|
509
|
+
print(match)
|
|
495
510
|
description = match.group(1)
|
|
496
511
|
url = match.group(2)
|
|
497
512
|
return f'{LINK[0]}{url}\033\\{Style.Link}{description}{UNDERLINE[1]}{LINK[1]}{FGRESET}'
|
|
498
513
|
|
|
499
|
-
|
|
500
|
-
|
|
514
|
+
if state.Images:
|
|
515
|
+
line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
|
|
516
|
+
|
|
517
|
+
if state.Links:
|
|
518
|
+
line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
|
|
519
|
+
|
|
501
520
|
line = re.sub(r"\[\^(\d+)\]:?", footnotes, line)
|
|
502
521
|
|
|
503
522
|
tokenList = re.finditer(r"((~~|\*\*_|_\*\*|\*{1,3}|_{1,3}|`+)|[^~_*`]+)", line)
|
|
@@ -630,8 +649,7 @@ def parse(stream):
|
|
|
630
649
|
line = state.buffer.decode('utf-8').replace('\t',' ')
|
|
631
650
|
state.has_newline = line.endswith('\n')
|
|
632
651
|
|
|
633
|
-
|
|
634
|
-
state.maybe_prompt = not state.has_newline and state.current()['none'] and re.match(r'^.*>\s+$', visible(line))
|
|
652
|
+
state.maybe_prompt = not state.has_newline and state.current()['none'] and re.match(state.prompt_regex, visible(line))
|
|
635
653
|
|
|
636
654
|
# let's wait for a newline
|
|
637
655
|
if state.maybe_prompt:
|
|
@@ -658,19 +676,27 @@ def parse(stream):
|
|
|
658
676
|
"""
|
|
659
677
|
|
|
660
678
|
# running this here avoids stray |
|
|
661
|
-
|
|
679
|
+
# So kimi doesn't newline after the <think> token and it uses some unicode triangle?. They'll
|
|
680
|
+
# newline at the end of it, but not the beginning.
|
|
681
|
+
block_match = re.match(r"^\s*((>\s*)+|[◁<].?think[>▷])(.*)", line)
|
|
662
682
|
if not state.in_code and block_match:
|
|
663
|
-
|
|
683
|
+
# wtf is this you might ask! Not all thinking models use < and > ...
|
|
684
|
+
# because why make life easy?
|
|
685
|
+
if block_match.group(1)[1:7] == '/think':
|
|
686
|
+
line = ''
|
|
664
687
|
state.block_depth = 0
|
|
665
688
|
yield RESET
|
|
666
|
-
elif block_match.group(1) == '
|
|
689
|
+
elif block_match.group(1)[1:6] == 'think':
|
|
690
|
+
line = block_match.group(3)
|
|
667
691
|
state.block_depth = 1
|
|
692
|
+
state.block_type = 'think'
|
|
668
693
|
else:
|
|
669
|
-
state.block_depth = block_match.group(
|
|
694
|
+
state.block_depth = block_match.group(1).count('>')
|
|
695
|
+
state.block_type = '>'
|
|
670
696
|
# we also need to consume those tokens
|
|
671
|
-
line = line[len(block_match.group(
|
|
697
|
+
line = line[len(block_match.group(1)):]
|
|
672
698
|
else:
|
|
673
|
-
if state.block_depth > 0:
|
|
699
|
+
if state.block_type == '>' and state.block_depth > 0:
|
|
674
700
|
yield FGRESET
|
|
675
701
|
state.block_depth = 0
|
|
676
702
|
|
|
@@ -839,7 +865,7 @@ def parse(stream):
|
|
|
839
865
|
while parts[-1] in [FGRESET, FORMATRESET]:
|
|
840
866
|
parts.pop()
|
|
841
867
|
|
|
842
|
-
tline_len = visible_length(tline)
|
|
868
|
+
tline_len = visible_length(tline.rstrip('\r\n'))
|
|
843
869
|
|
|
844
870
|
# now we find the new stuff:
|
|
845
871
|
ttl = 0
|
|
@@ -850,7 +876,7 @@ def parse(stream):
|
|
|
850
876
|
|
|
851
877
|
ttl += len(idx) if idx[0] != '\x1b' else 0
|
|
852
878
|
|
|
853
|
-
if ttl >
|
|
879
|
+
if ttl > tline_len:
|
|
854
880
|
break
|
|
855
881
|
|
|
856
882
|
|
|
@@ -1011,6 +1037,11 @@ def parse(stream):
|
|
|
1011
1037
|
for wrapped_line in wrapped_lines:
|
|
1012
1038
|
yield f"{state.space_left()}{wrapped_line}\n"
|
|
1013
1039
|
|
|
1040
|
+
def terminal_prep(what):
|
|
1041
|
+
if Style.Plaintext:
|
|
1042
|
+
return strip_ansi(what)
|
|
1043
|
+
return what
|
|
1044
|
+
|
|
1014
1045
|
def emit(inp):
|
|
1015
1046
|
buffer = []
|
|
1016
1047
|
flush = False
|
|
@@ -1050,10 +1081,10 @@ def emit(inp):
|
|
|
1050
1081
|
else:
|
|
1051
1082
|
chunk = buffer.pop(0)
|
|
1052
1083
|
|
|
1053
|
-
print(chunk, end="", file=sys.stdout, flush=True)
|
|
1084
|
+
print(terminal_prep(chunk), end="", file=sys.stdout, flush=True)
|
|
1054
1085
|
|
|
1055
1086
|
if len(buffer):
|
|
1056
|
-
print(buffer.pop(0), file=sys.stdout, end="", flush=True)
|
|
1087
|
+
print(terminal_prep(buffer.pop(0)), file=sys.stdout, end="", flush=True)
|
|
1057
1088
|
|
|
1058
1089
|
def ansi2hex(ansi_code):
|
|
1059
1090
|
parts = ansi_code.strip('m').split(";")
|
|
@@ -1109,8 +1140,10 @@ def main():
|
|
|
1109
1140
|
parser.add_argument("-c", "--config", default=None, help="Use a custom config override")
|
|
1110
1141
|
parser.add_argument("-w", "--width", default="0", help="Set the width WIDTH")
|
|
1111
1142
|
parser.add_argument("-e", "--exec", help="Wrap a program EXEC for more 'proper' i/o handling")
|
|
1143
|
+
parser.add_argument("-p", "--prompt", default="^.*>\\s+$", help="A PCRE regex prompt to detect (default: %(default)s)")
|
|
1112
1144
|
parser.add_argument("-s", "--scrape", help="Scrape code snippets to a directory SCRAPE")
|
|
1113
1145
|
parser.add_argument("-v", "--version", action="store_true", help="Show version information")
|
|
1146
|
+
parser.add_argument("--strip", action="store_true", help="Just strip the markdown and output plaintext")
|
|
1114
1147
|
args = parser.parse_args()
|
|
1115
1148
|
|
|
1116
1149
|
if args.version:
|
|
@@ -1140,9 +1173,9 @@ def main():
|
|
|
1140
1173
|
|
|
1141
1174
|
for color in ["Dark", "Mid", "Symbol", "Head", "Grey", "Bright"]:
|
|
1142
1175
|
setattr(Style, color, apply_multipliers(style, color, H, S, V))
|
|
1143
|
-
for attr in ['PrettyPad', 'PrettyBroken', 'Margin', 'ListIndent', 'Syntax']:
|
|
1176
|
+
for attr in ['PrettyPad', 'PrettyBroken', 'Margin', 'ListIndent', 'Syntax', 'Plaintext']:
|
|
1144
1177
|
setattr(Style, attr, style.get(attr))
|
|
1145
|
-
for attr in ['CodeSpaces', 'Clipboard', 'Logging', 'Timeout', 'Savebrace']:
|
|
1178
|
+
for attr in ['Links', 'Images', 'CodeSpaces', 'Clipboard', 'Logging', 'Timeout', 'Savebrace']:
|
|
1146
1179
|
setattr(state, attr, features.get(attr))
|
|
1147
1180
|
|
|
1148
1181
|
|
|
@@ -1150,8 +1183,10 @@ def main():
|
|
|
1150
1183
|
os.makedirs(args.scrape, exist_ok=True)
|
|
1151
1184
|
state.scrape = args.scrape
|
|
1152
1185
|
|
|
1186
|
+
Style.Plaintext = args.strip
|
|
1153
1187
|
Style.MarginSpaces = " " * Style.Margin
|
|
1154
1188
|
state.WidthArg = int(args.width) or style.get("Width") or 0
|
|
1189
|
+
state.prompt_regex = re.compile(args.prompt)
|
|
1155
1190
|
Style.Blockquote = f"{FG}{Style.Grey}│ "
|
|
1156
1191
|
width_calc()
|
|
1157
1192
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: streamdown
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.35.1
|
|
4
4
|
Summary: A streaming markdown renderer for modern terminals with syntax highlighting
|
|
5
5
|
Project-URL: Homepage, https://github.com/day50-dev/Streamdown
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/day50-dev/Streamdown/issues
|
|
@@ -70,7 +70,7 @@ The optional `Clipboard` feature puts the final codeblock into your clipboard. S
|
|
|
70
70
|
[links.webm](https://github.com/user-attachments/assets/a5f71791-7c58-4183-ad3b-309f470c08a3)
|
|
71
71
|
|
|
72
72
|
### As well as everything else...
|
|
73
|
-
Here's the `Savebrace` feature with `
|
|
73
|
+
Here's the `Savebrace` feature with [`sidechat` and `sc-picker`](https://github.com/day50-dev/sidechat). You can have an ongoing conversation in tmux with your terminal session. Then use popups and fzf to insert command or coding blocks all with a keystroke.
|
|
74
74
|
|
|
75
75
|
This allows you to interactively debug in a way that the agent doesn't just wander off doing silly things.
|
|
76
76
|
|
|
@@ -156,6 +156,8 @@ $ sd --exec "llm chat"
|
|
|
156
156
|
|
|
157
157
|
And now you have all your readline stuff. It's pretty great. (Also see the DAY50 shellwrap project.)
|
|
158
158
|
|
|
159
|
+
This relies on "guessing" what a prompt will look like. But don't worry, you can change that with the `--prompt` option if yours is a bit unique. It's a PCRE regex, so you can be a bit flexible.
|
|
160
|
+
|
|
159
161
|
It's also worth noting that things like the `-c` aren't "broken" with regard to file input. You can do something like this:
|
|
160
162
|
|
|
161
163
|
```shell
|
|
@@ -188,6 +190,8 @@ optional arguments:
|
|
|
188
190
|
-w WIDTH, --width WIDTH
|
|
189
191
|
Set the width WIDTH
|
|
190
192
|
-e EXEC, --exec EXEC Wrap a program EXEC for more 'proper' i/o handling
|
|
193
|
+
-p PROMPT, --prompt PROMPT
|
|
194
|
+
A PCRE regex prompt to detect (default: ^.*>\s+$)
|
|
191
195
|
-s SCRAPE, --scrape SCRAPE
|
|
192
196
|
Scrape code snippets to a directory SCRAPE
|
|
193
197
|
-v, --version Show version information
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
streamdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
streamdown/qwen3.md,sha256=1e7ELkK-quwUeOmBDwXodFH-DlnfAcQWj32rjK6Zex4,542
|
|
3
|
-
streamdown/sd.py,sha256=
|
|
3
|
+
streamdown/sd.py,sha256=4huBv890pt3QOcwUnhUkRSHOus_E-8QSEBf2D2LAJdQ,46604
|
|
4
4
|
streamdown/ss,sha256=sel_phpaecrw6WGIHRLROsD7BFShf0rSDHheflwdUn8,277
|
|
5
5
|
streamdown/ss1,sha256=CUVf86_2zeAle2oQCeTfWYqtHBrAFR_UgvptuYMQzFU,3151
|
|
6
6
|
streamdown/test.txt,sha256=j0NDRmSPa5bfid2pAcUXaxCm2Dlh3TwayItZstwyeqQ,2
|
|
7
7
|
streamdown/plugins/README.md,sha256=KWqYELs9WkKJmuDzYv3cvPlZMkArsNCBUe4XDoTLjLA,1143
|
|
8
8
|
streamdown/plugins/latex.py,sha256=xZMGMdx_Sw4X1piZejXFHfEG9qazU4fGeceiMI0h13Y,648
|
|
9
|
-
streamdown-0.
|
|
10
|
-
streamdown-0.
|
|
11
|
-
streamdown-0.
|
|
12
|
-
streamdown-0.
|
|
13
|
-
streamdown-0.
|
|
9
|
+
streamdown-0.35.1.dist-info/METADATA,sha256=n9vnRuHZpIYmiHtqJ-2nWFWD93UCJQjo7Z0EOhaIES4,10494
|
|
10
|
+
streamdown-0.35.1.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
11
|
+
streamdown-0.35.1.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
|
|
12
|
+
streamdown-0.35.1.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
|
|
13
|
+
streamdown-0.35.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|