streamdown 0.18.0__py3-none-any.whl → 0.20.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.
- streamdown/sd.py +100 -28
- {streamdown-0.18.0.dist-info → streamdown-0.20.0.dist-info}/METADATA +25 -15
- {streamdown-0.18.0.dist-info → streamdown-0.20.0.dist-info}/RECORD +6 -6
- {streamdown-0.18.0.dist-info → streamdown-0.20.0.dist-info}/WHEEL +0 -0
- {streamdown-0.18.0.dist-info → streamdown-0.20.0.dist-info}/entry_points.txt +0 -0
- {streamdown-0.18.0.dist-info → streamdown-0.20.0.dist-info}/licenses/LICENSE.MIT +0 -0
streamdown/sd.py
CHANGED
|
@@ -65,7 +65,7 @@ Symbol = { H = 1.00, S = 1.00, V = 1.50 }
|
|
|
65
65
|
Head = { H = 1.00, S = 1.00, V = 1.75 }
|
|
66
66
|
Grey = { H = 1.00, S = 0.25, V = 1.37 }
|
|
67
67
|
Bright = { H = 1.00, S = 2.00, V = 2.00 }
|
|
68
|
-
Syntax = "
|
|
68
|
+
Syntax = "dracula"
|
|
69
69
|
"""
|
|
70
70
|
|
|
71
71
|
def ensure_config_file():
|
|
@@ -92,6 +92,7 @@ BOLD = ["\033[1m", "\033[22m"]
|
|
|
92
92
|
UNDERLINE = ["\033[4m", "\033[24m"]
|
|
93
93
|
ITALIC = ["\033[3m", "\033[23m"]
|
|
94
94
|
STRIKEOUT = ["\033[9m", "\033[29m"]
|
|
95
|
+
LINK = ["\033]8;;", "\033]8;;\033\\"]
|
|
95
96
|
SUPER = [ 0x2070, 0x00B9, 0x00B2, 0x00B3, 0x2074, 0x2075, 0x2076, 0x2077, 0x2078, 0x2079 ]
|
|
96
97
|
|
|
97
98
|
ESCAPE = r"\033\[[0-9;]*[mK]"
|
|
@@ -100,7 +101,7 @@ KEYCODE_RE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
|
100
101
|
|
|
101
102
|
visible = lambda x: re.sub(ANSIESCAPE, "", x)
|
|
102
103
|
# cjk characters are double width
|
|
103
|
-
visible_length = lambda x: len(visible(x)) +
|
|
104
|
+
visible_length = lambda x: len(visible(x)) + dbl_count(x)
|
|
104
105
|
extract_ansi_codes = lambda text: re.findall(ESCAPE, text)
|
|
105
106
|
remove_ansi = lambda line, codeList: reduce(lambda line, code: line.replace(code, ''), codeList, line)
|
|
106
107
|
|
|
@@ -116,8 +117,8 @@ def savebrace():
|
|
|
116
117
|
if state.Savebrace and state.code_buffer_raw:
|
|
117
118
|
path = os.path.join(tempfile.gettempdir(), "sd", 'savebrace')
|
|
118
119
|
with open(path, "a") as f:
|
|
119
|
-
f.write(state.code_buffer_raw)
|
|
120
|
-
|
|
120
|
+
f.write(state.code_buffer_raw + "\x00")
|
|
121
|
+
f.flush()
|
|
121
122
|
|
|
122
123
|
class Goto(Exception):
|
|
123
124
|
pass
|
|
@@ -208,7 +209,7 @@ class ParseState:
|
|
|
208
209
|
return offset + (state.current_width(listwidth = True) if Style.PrettyBroken else self.WidthFull)
|
|
209
210
|
|
|
210
211
|
def current_width(self, listwidth = False):
|
|
211
|
-
return self.Width - (len(visible(self.space_left(listwidth)))
|
|
212
|
+
return self.Width - (len(visible(self.space_left(listwidth))))
|
|
212
213
|
|
|
213
214
|
def space_left(self, listwidth = False):
|
|
214
215
|
pre = ' ' * (len(state.list_item_stack)) * Style.ListIndent if listwidth else ''
|
|
@@ -216,6 +217,20 @@ class ParseState:
|
|
|
216
217
|
|
|
217
218
|
state = ParseState()
|
|
218
219
|
|
|
220
|
+
def override_background(style_name, background_color):
|
|
221
|
+
base_style = get_style_by_name(style_name)
|
|
222
|
+
base_style.background_color = background_color
|
|
223
|
+
for i in base_style:
|
|
224
|
+
i[1]['bgcolor'] = background_color
|
|
225
|
+
for i,v in base_style.styles.items():
|
|
226
|
+
if v and 'bg' in v:
|
|
227
|
+
base_style.styles[i] = re.sub(r'bg:[^ ]*', '', base_style.styles[i] )
|
|
228
|
+
for k,v in base_style._styles.items():
|
|
229
|
+
if v[4] != '':
|
|
230
|
+
v[4] = ''
|
|
231
|
+
|
|
232
|
+
return base_style
|
|
233
|
+
|
|
219
234
|
def format_table(rowList):
|
|
220
235
|
num_cols = len(rowList)
|
|
221
236
|
row_height = 0
|
|
@@ -224,15 +239,20 @@ def format_table(rowList):
|
|
|
224
239
|
# Calculate max width per column (integer division)
|
|
225
240
|
# Subtract num_cols + 1 for the vertical borders '│'
|
|
226
241
|
available_width = state.current_width() - (num_cols + 1)
|
|
227
|
-
|
|
242
|
+
|
|
243
|
+
width_base = available_width // num_cols
|
|
244
|
+
width_mod = available_width % num_cols
|
|
245
|
+
|
|
246
|
+
col_width_list = [width_base + (1 if i < width_mod else 0) for i in range(num_cols)]
|
|
228
247
|
bg_color = Style.Mid if state.in_table == Style.Head else Style.Dark
|
|
229
248
|
state.bg = f"{BG}{bg_color}"
|
|
230
249
|
|
|
231
250
|
# First Pass: Wrap text and calculate row heights
|
|
232
251
|
# Note this is where every cell is formatted so if
|
|
233
252
|
# you are styling, do it before here!
|
|
234
|
-
for
|
|
235
|
-
|
|
253
|
+
for ix in range(len(rowList)):
|
|
254
|
+
row = rowList[ix]
|
|
255
|
+
wrapped_cell = text_wrap(row, width=col_width_list[ix], force_truncate=True)
|
|
236
256
|
|
|
237
257
|
# Ensure at least one line, even for empty cells
|
|
238
258
|
if not wrapped_cell:
|
|
@@ -248,13 +268,14 @@ def format_table(rowList):
|
|
|
248
268
|
line_segments = []
|
|
249
269
|
|
|
250
270
|
# Now we want to snatch this row index from all our cells
|
|
251
|
-
for
|
|
271
|
+
for iy in range(len(wrapped_cellList)):
|
|
272
|
+
cell = wrapped_cellList[iy]
|
|
252
273
|
segment = ''
|
|
253
274
|
if ix < len(cell):
|
|
254
275
|
segment = cell[ix]
|
|
255
276
|
|
|
256
277
|
# Margin logic is correctly indented here
|
|
257
|
-
margin_needed =
|
|
278
|
+
margin_needed = col_width_list[iy] - visible_length(segment)
|
|
258
279
|
margin_segment = segment + (" " * max(0, margin_needed))
|
|
259
280
|
line_segments.append(f"{BG}{bg_color}{extra} {margin_segment}")
|
|
260
281
|
|
|
@@ -269,7 +290,7 @@ def emit_h(level, text):
|
|
|
269
290
|
text = line_format(text)
|
|
270
291
|
spaces_to_center = (state.current_width() - visible_length(text)) / 2
|
|
271
292
|
if level == 1: #
|
|
272
|
-
return f"{state.space_left()}\n{state.space_left()}{BOLD[
|
|
293
|
+
return f"{state.space_left()}\n{state.space_left()}{BOLD[1]}{' ' * math.floor(spaces_to_center)}{text}{BOLD[1]}"
|
|
273
294
|
elif level == 2: ##
|
|
274
295
|
return f"{state.space_left()}\n{state.space_left()}{BOLD[0]}{FG}{Style.Bright}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{BOLD[1]}{FGRESET}"
|
|
275
296
|
elif level == 3: ###
|
|
@@ -337,21 +358,35 @@ def ansi_collapse(codelist, inp):
|
|
|
337
358
|
|
|
338
359
|
|
|
339
360
|
def split_text(text):
|
|
340
|
-
return re.split(
|
|
341
|
-
r'(?<=[
|
|
361
|
+
return [x for x in re.split(
|
|
362
|
+
r'(?<=['
|
|
363
|
+
r'\u3000-\u303F'
|
|
364
|
+
r'\u4E00-\u9FFF'
|
|
365
|
+
r'\u3400-\u4DBF'
|
|
366
|
+
r'\uF900-\uFAFF'
|
|
367
|
+
r'])|(?=['
|
|
368
|
+
#r'\u4E00-\u9FFF'
|
|
369
|
+
r'\u3400-\u4DBF'
|
|
370
|
+
r'\uF900-\uFAFF'
|
|
371
|
+
r'])|\s+',
|
|
342
372
|
text
|
|
343
|
-
)
|
|
373
|
+
) if x]
|
|
344
374
|
|
|
345
375
|
def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_line_prefix="", force_truncate=False):
|
|
346
376
|
if width == -1:
|
|
347
377
|
width = state.Width
|
|
348
378
|
|
|
349
379
|
# The empty word clears the buffer at the end.
|
|
350
|
-
|
|
380
|
+
formatted = line_format(text)
|
|
381
|
+
#print(bytes(formatted, 'utf-8'), formatted)
|
|
382
|
+
words = split_text(formatted) + [""]
|
|
383
|
+
#print([bytes(i, 'utf-8') for i in words])
|
|
384
|
+
|
|
351
385
|
lines = []
|
|
352
386
|
current_line = ""
|
|
353
387
|
current_style = []
|
|
354
388
|
|
|
389
|
+
oldword = ''
|
|
355
390
|
for word in words:
|
|
356
391
|
# we apply the style if we see it at the beginning of the word
|
|
357
392
|
codes = extract_ansi_codes(word)
|
|
@@ -360,8 +395,12 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
|
|
|
360
395
|
current_style.append(codes.pop(0))
|
|
361
396
|
|
|
362
397
|
if len(word) and visible_length(current_line) + visible_length(word) + 1 <= width: # +1 for space
|
|
363
|
-
|
|
364
|
-
|
|
398
|
+
space = ""
|
|
399
|
+
if len(visible(word)) > 0 and current_line:
|
|
400
|
+
space = " "
|
|
401
|
+
if (":" in visible(word) or cjk_count(word)) and cjk_count(oldword):
|
|
402
|
+
space = ""
|
|
403
|
+
current_line += space + word
|
|
365
404
|
else:
|
|
366
405
|
# Word doesn't fit, finalize the previous line
|
|
367
406
|
prefix = first_line_prefix if not lines else subsequent_line_prefix
|
|
@@ -373,6 +412,10 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
|
|
|
373
412
|
margin = max(0, width - visible_length(line_content))
|
|
374
413
|
|
|
375
414
|
if line_content.strip() != "":
|
|
415
|
+
# We make absolutely positively sure beyond any doubt
|
|
416
|
+
# that we have closed our hyperlink OSC
|
|
417
|
+
if LINK[0] in line_content:
|
|
418
|
+
line_content += LINK[1]
|
|
376
419
|
lines.append(line_content + state.bg + ' ' * margin)
|
|
377
420
|
|
|
378
421
|
current_line = (" " * indent) + "".join(current_style) + word
|
|
@@ -383,19 +426,30 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
|
|
|
383
426
|
if codes:
|
|
384
427
|
current_style = ansi_collapse(current_style, codes)
|
|
385
428
|
|
|
429
|
+
oldword = word
|
|
430
|
+
|
|
386
431
|
if len(lines) < 1:
|
|
387
432
|
return []
|
|
388
433
|
|
|
389
434
|
return lines
|
|
390
435
|
|
|
436
|
+
def dbl_count(s):
|
|
437
|
+
dbl_re = re.compile(
|
|
438
|
+
r'[\u2e80-\u2eff\u3000-\u303f\u3400-\u4dbf'
|
|
439
|
+
r'\U00004e00-\U00009fff\U0001f300-\U0001f6ff'
|
|
440
|
+
r'\U0001f900-\U0001f9ff\U0001fa70-\U0001faff]',
|
|
441
|
+
re.UNICODE
|
|
442
|
+
)
|
|
443
|
+
return len(dbl_re.findall(visible(s)))
|
|
444
|
+
|
|
391
445
|
def cjk_count(s):
|
|
392
446
|
cjk_re = re.compile(
|
|
393
447
|
r'[\u4E00-\u9FFF' # CJK Unified Ideographs
|
|
394
|
-
r'
|
|
395
|
-
r'
|
|
396
|
-
r'
|
|
397
|
-
r'
|
|
398
|
-
r'
|
|
448
|
+
r'\u3400-\u4DBF' # CJK Unified Ideographs Extension A
|
|
449
|
+
r'\uF900-\uFAFF' # CJK Compatibility Ideographs
|
|
450
|
+
r'\uFF00-\uFFEF' # CJK Compatibility Punctuation
|
|
451
|
+
r'\u3000-\u303F' # CJK Symbols and Punctuation
|
|
452
|
+
r'\U0002F800-\U0002FA1F]' # CJK Compatibility Ideographs Supplement
|
|
399
453
|
)
|
|
400
454
|
|
|
401
455
|
return len(cjk_re.findall(visible(s)))
|
|
@@ -420,7 +474,7 @@ def line_format(line):
|
|
|
420
474
|
def process_links(match):
|
|
421
475
|
description = match.group(1)
|
|
422
476
|
url = match.group(2)
|
|
423
|
-
return f'
|
|
477
|
+
return f'{LINK[0]}{url}\033\\{Style.Link}{description}{UNDERLINE[1]}{LINK[1]}{FGRESET}'
|
|
424
478
|
|
|
425
479
|
line = re.sub(r"\!\[([^\]]*)\]\(([^\)]+)\)", process_images, line)
|
|
426
480
|
line = re.sub(r"\[([^\]]+)\]\(([^\)]+)\)", process_links, line)
|
|
@@ -437,19 +491,24 @@ def line_format(line):
|
|
|
437
491
|
# This trick makes sure that things like `` ` `` render right.
|
|
438
492
|
if "`" in token and (not state.inline_code or state.inline_code == token):
|
|
439
493
|
if state.inline_code:
|
|
494
|
+
if ' ' in state.inline_code:
|
|
495
|
+
savebrace()
|
|
440
496
|
state.inline_code = False
|
|
441
497
|
else:
|
|
442
498
|
state.inline_code = token
|
|
499
|
+
state.code_buffer_raw = ''
|
|
443
500
|
|
|
444
501
|
if state.inline_code:
|
|
445
502
|
result += f'{BG}{Style.Mid}'
|
|
446
503
|
else:
|
|
447
504
|
result += state.bg
|
|
505
|
+
state.code_buffer_raw = ''
|
|
448
506
|
|
|
449
507
|
# This is important here because we ignore formatting
|
|
450
508
|
# inside of our code block.
|
|
451
509
|
elif state.inline_code:
|
|
452
510
|
result += token
|
|
511
|
+
state.code_buffer_raw += token
|
|
453
512
|
|
|
454
513
|
elif token == '~~' and (state.in_strikeout or not_text(prev_token)):
|
|
455
514
|
state.in_strikeout = not state.in_strikeout
|
|
@@ -552,6 +611,7 @@ def parse(stream):
|
|
|
552
611
|
continue
|
|
553
612
|
|
|
554
613
|
state.buffer = b''
|
|
614
|
+
"""
|
|
555
615
|
# Run through the plugins first
|
|
556
616
|
res = latex.Plugin(line, state, Style)
|
|
557
617
|
if res is True:
|
|
@@ -562,6 +622,7 @@ def parse(stream):
|
|
|
562
622
|
for row in res:
|
|
563
623
|
yield row
|
|
564
624
|
continue
|
|
625
|
+
"""
|
|
565
626
|
|
|
566
627
|
# running this here avoids stray |
|
|
567
628
|
block_match = re.match(r"^\s*((>\s*)+|<.?think>)", line)
|
|
@@ -632,7 +693,6 @@ def parse(stream):
|
|
|
632
693
|
state.code_language = 'Bash'
|
|
633
694
|
|
|
634
695
|
if state.in_code:
|
|
635
|
-
savebrace()
|
|
636
696
|
state.code_buffer = state.code_buffer_raw = ""
|
|
637
697
|
state.code_gen = 0
|
|
638
698
|
state.code_first_line = True
|
|
@@ -664,6 +724,7 @@ def parse(stream):
|
|
|
664
724
|
open(os.path.join(state.scrape, f"file_{state.scrape_ix}.{ext}"), 'w').write(state.code_buffer_raw)
|
|
665
725
|
state.scrape_ix += 1
|
|
666
726
|
|
|
727
|
+
savebrace()
|
|
667
728
|
state.code_language = None
|
|
668
729
|
state.code_indent = 0
|
|
669
730
|
code_type = state.in_code
|
|
@@ -691,10 +752,10 @@ def parse(stream):
|
|
|
691
752
|
state.code_first_line = False
|
|
692
753
|
try:
|
|
693
754
|
lexer = get_lexer_by_name(state.code_language)
|
|
694
|
-
custom_style =
|
|
755
|
+
custom_style = override_background(Style.Syntax, ansi2hex(Style.Dark))
|
|
695
756
|
except pygments.util.ClassNotFound:
|
|
696
757
|
lexer = get_lexer_by_name("Bash")
|
|
697
|
-
custom_style =
|
|
758
|
+
custom_style = override_background("default", ansi2hex(Style.Dark))
|
|
698
759
|
|
|
699
760
|
formatter = TerminalTrueColorFormatter(style=custom_style)
|
|
700
761
|
if line.startswith(' ' * state.code_indent):
|
|
@@ -713,6 +774,7 @@ def parse(stream):
|
|
|
713
774
|
else:
|
|
714
775
|
continue
|
|
715
776
|
|
|
777
|
+
highlighted_code = highlight(line, lexer, formatter)
|
|
716
778
|
indent, line_wrap = code_wrap(line)
|
|
717
779
|
|
|
718
780
|
state.where_from = "in code"
|
|
@@ -724,15 +786,18 @@ def parse(stream):
|
|
|
724
786
|
# then naively search back until our visible_lengths() match. This is not fast and there's certainly smarter
|
|
725
787
|
# ways of doing it but this thing is way trickery than you think
|
|
726
788
|
highlighted_code = highlight(state.code_buffer + tline, lexer, formatter)
|
|
789
|
+
#print("(",highlighted_code,")")
|
|
727
790
|
|
|
728
791
|
# Sometimes the highlighter will do things like a full reset or a background reset.
|
|
729
792
|
# This is not what we want
|
|
730
|
-
highlighted_code = re.sub(r"\033\[
|
|
793
|
+
highlighted_code = re.sub(r"\033\[[34]9(;00|)m", '', highlighted_code)
|
|
731
794
|
|
|
732
795
|
# Since we are streaming we ignore the resets and newlines at the end
|
|
733
796
|
if highlighted_code.endswith(FGRESET + "\n"):
|
|
734
797
|
highlighted_code = highlighted_code[: -(1 + len(FGRESET))]
|
|
735
798
|
|
|
799
|
+
#print(bytes(highlighted_code, 'utf-8'))
|
|
800
|
+
|
|
736
801
|
# turns out highlight will eat leading newlines on empty lines
|
|
737
802
|
vislen = visible_length(state.code_buffer.lstrip())
|
|
738
803
|
|
|
@@ -834,7 +899,7 @@ def parse(stream):
|
|
|
834
899
|
# This is intentional ... we can get here in llama 4 using
|
|
835
900
|
# a weird thing
|
|
836
901
|
if state.in_list:
|
|
837
|
-
indent = (len(state.list_item_stack) - 1) * Style.ListIndent
|
|
902
|
+
indent = (len(state.list_item_stack) - 1) * Style.ListIndent #+ (len(bullet) - 1)
|
|
838
903
|
wrap_width = state.current_width() - indent - (2 * Style.ListIndent)
|
|
839
904
|
|
|
840
905
|
wrapped_lineList = text_wrap(content, wrap_width, Style.ListIndent,
|
|
@@ -920,6 +985,11 @@ def emit(inp):
|
|
|
920
985
|
if len(buffer):
|
|
921
986
|
print(buffer.pop(0), file=sys.stdout, end="", flush=True)
|
|
922
987
|
|
|
988
|
+
def ansi2hex(ansi_code):
|
|
989
|
+
parts = ansi_code.strip('m').split(";")
|
|
990
|
+
r, g, b = map(int, parts)
|
|
991
|
+
return f"#{r:02x}{g:02x}{b:02x}"
|
|
992
|
+
|
|
923
993
|
def apply_multipliers(name, H, S, V):
|
|
924
994
|
m = _style.get(name)
|
|
925
995
|
r, g, b = colorsys.hsv_to_rgb(min(1.0, H * m["H"]), min(1.0, S * m["S"]), min(1.0, V * m["V"]))
|
|
@@ -1035,6 +1105,8 @@ def main():
|
|
|
1035
1105
|
os.close(state.exec_master)
|
|
1036
1106
|
if state.exec_sub:
|
|
1037
1107
|
state.exec_sub.wait()
|
|
1108
|
+
|
|
1109
|
+
print(RESET, end="")
|
|
1038
1110
|
sys.exit(state.exit)
|
|
1039
1111
|
|
|
1040
1112
|
if __name__ == "__main__":
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: streamdown
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.20.0
|
|
4
4
|
Summary: A streaming markdown renderer for modern terminals with syntax highlighting
|
|
5
5
|
Project-URL: Homepage, https://github.com/kristopolous/Streamdown
|
|
6
6
|
Project-URL: Bug Tracker, https://github.com/kristopolous/Streamdown/issues
|
|
@@ -36,43 +36,51 @@ Description-Content-Type: text/markdown
|
|
|
36
36
|
</p>
|
|
37
37
|
|
|
38
38
|
|
|
39
|
-
Streamdown works with [simonw's llm](https://github.com/simonw/llm)
|
|
40
|
-
|
|
39
|
+
Streamdown works with any streaming markdown such as [simonw's llm](https://github.com/simonw/llm) or even something basic like curl.
|
|
40
|
+
|
|
41
|
+
It supports standard piping and files as arguments like any normal pager but can also run as a wrapper so you retain full keyboard interactivity. Arrow keys, control, alt, all still work.
|
|
41
42
|
```bash
|
|
42
43
|
$ pip install streamdown
|
|
43
44
|
```
|
|
44
45
|

|
|
45
46
|
|
|
46
47
|
### Provides clean copyable code for long code lines
|
|
47
|
-
|
|
48
|
+
Other renderers inject line breaks when copying code that wraps around. Streamdown's better and now you are too!
|
|
48
49
|

|
|
49
|
-
**Tip**: You can make things prettier if you don't mind if this guarantee is broken. See the `PrettyBroken` flag below!
|
|
50
|
+
**Tip**: You can make things prettier if you don't mind if this guarantee is broken. See the `PrettyBroken` flag below! (There's still 2 other convenient ways of getting code blocks out.)
|
|
50
51
|
|
|
51
52
|
### Supports images
|
|
52
|
-
Here's kitty and alacritty.
|
|
53
|
+
Here's kitty and alacritty.
|
|
53
54
|

|
|
54
55
|
|
|
55
56
|
### Supports hyperlinks (OSC 8) and clipboard (OSC 52)
|
|
57
|
+
The optional `Clipboard` feature puts the final codeblock into your clipboard. See below for details.
|
|
58
|
+
|
|
56
59
|
[links.webm](https://github.com/user-attachments/assets/a5f71791-7c58-4183-ad3b-309f470c08a3)
|
|
57
60
|
|
|
58
|
-
###
|
|
59
|
-
|
|
61
|
+
### As well as everything else...
|
|
62
|
+
Here's the `Savebrace` feature with `screen-query` and `sd-picker` from [llmehelp](https://github.com/kristopolous/llmehelp). 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.
|
|
63
|
+
|
|
64
|
+
This allows you to interactively debug in a way that the agent doesn't just wander off doing silly things.
|
|
60
65
|
|
|
61
|
-
|
|
62
|
-

|
|
63
68
|
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
### ...It even supports CJK
|
|
70
|
+
Compare how streamdown wraps and spaces this tabular Chinese description of programming languages to other leading markdown renderers.
|
|
71
|
+
|
|
72
|
+
Only one generates the text without truncation. 很美!
|
|
73
|
+

|
|
66
74
|
|
|
67
75
|
### Colors are highly (and quickly) configurable for people who care a lot, or just a little.
|
|
68
76
|

|
|
69
77
|
|
|
70
|
-
### Has a [Plugin](https://github.com/kristopolous/Streamdown/tree/main/streamdown/plugins) system to extend the parser and
|
|
78
|
+
### Has a [Plugin](https://github.com/kristopolous/Streamdown/tree/main/streamdown/plugins) system to extend the parser and renderers.
|
|
71
79
|
For instance, here is the [latex plugin](https://github.com/kristopolous/Streamdown/blob/main/streamdown/plugins/latex.py) doing math inside a table:
|
|
72
80
|

|
|
73
81
|
|
|
74
82
|
|
|
75
|
-
##
|
|
83
|
+
## Configuration
|
|
76
84
|
|
|
77
85
|
It's located at `~/.config/streamdown/config.toml` (following the XDG Base Directory Specification). If this file does not exist upon first run, it will be created with default values.
|
|
78
86
|
|
|
@@ -80,7 +88,9 @@ Here are the sections:
|
|
|
80
88
|
|
|
81
89
|
**`[style]`**
|
|
82
90
|
|
|
83
|
-
Defines the base Hue (H), Saturation (S), and Value (V) from which all other palette colors are derived.
|
|
91
|
+
Defines the base Hue (H), Saturation (S), and Value (V) from which all other palette colors are derived. This can also be specified at runtime via command line arguments. See below!
|
|
92
|
+
|
|
93
|
+
The default values are [at the beginning of the source](https://github.com/kristopolous/Streamdown/blob/main/streamdown/sd.py#L33).
|
|
84
94
|
|
|
85
95
|
* `HSV`: [ 0.0 - 1.0, 0.0 - 1.0, 0.0 - 1.0 ]
|
|
86
96
|
* `Dark`: Multipliers for background elements, code blocks.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
streamdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
streamdown/sd.py,sha256=
|
|
2
|
+
streamdown/sd.py,sha256=ggXlCGovA6cS3686BLnk0ISzf2xXrx7UwuKjwjs6RUM,41475
|
|
3
3
|
streamdown/ss,sha256=sel_phpaecrw6WGIHRLROsD7BFShf0rSDHheflwdUn8,277
|
|
4
4
|
streamdown/ss1,sha256=CUVf86_2zeAle2oQCeTfWYqtHBrAFR_UgvptuYMQzFU,3151
|
|
5
5
|
streamdown/plugins/README.md,sha256=KWqYELs9WkKJmuDzYv3cvPlZMkArsNCBUe4XDoTLjLA,1143
|
|
6
6
|
streamdown/plugins/latex.py,sha256=xZMGMdx_Sw4X1piZejXFHfEG9qazU4fGeceiMI0h13Y,648
|
|
7
|
-
streamdown-0.
|
|
8
|
-
streamdown-0.
|
|
9
|
-
streamdown-0.
|
|
10
|
-
streamdown-0.
|
|
11
|
-
streamdown-0.
|
|
7
|
+
streamdown-0.20.0.dist-info/METADATA,sha256=LpTl5FFiZJMk_9OC9Kqqq72satHDEHn19rGnJ93XSKY,8843
|
|
8
|
+
streamdown-0.20.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
+
streamdown-0.20.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
|
|
10
|
+
streamdown-0.20.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
|
|
11
|
+
streamdown-0.20.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|