streamdown 0.27.0__py3-none-any.whl → 0.29.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 +76 -56
- {streamdown-0.27.0.dist-info → streamdown-0.29.0.dist-info}/METADATA +22 -11
- streamdown-0.29.0.dist-info/RECORD +9 -0
- streamdown/ss +0 -1
- streamdown/ss1 +0 -42
- streamdown-0.27.0.dist-info/RECORD +0 -11
- {streamdown-0.27.0.dist-info → streamdown-0.29.0.dist-info}/WHEEL +0 -0
- {streamdown-0.27.0.dist-info → streamdown-0.29.0.dist-info}/entry_points.txt +0 -0
- {streamdown-0.27.0.dist-info → streamdown-0.29.0.dist-info}/licenses/LICENSE.MIT +0 -0
streamdown/sd.py
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
# "pylatexenc",
|
|
7
7
|
# "appdirs",
|
|
8
8
|
# "term-image",
|
|
9
|
+
# "wcwidth",
|
|
9
10
|
# "toml"
|
|
10
11
|
# ]
|
|
11
12
|
# ///
|
|
@@ -25,13 +26,14 @@ import termios, tty
|
|
|
25
26
|
import math
|
|
26
27
|
import re
|
|
27
28
|
import shutil
|
|
28
|
-
import subprocess
|
|
29
29
|
import traceback
|
|
30
30
|
import colorsys
|
|
31
31
|
import base64
|
|
32
|
+
import subprocess
|
|
32
33
|
from io import BytesIO
|
|
33
34
|
from term_image.image import from_file, from_url
|
|
34
35
|
import pygments.util
|
|
36
|
+
from wcwidth import wcwidth
|
|
35
37
|
from functools import reduce
|
|
36
38
|
from argparse import ArgumentParser
|
|
37
39
|
from pygments import highlight
|
|
@@ -64,27 +66,34 @@ Mid = { H = 1.00, S = 1.00, V = 0.50 }
|
|
|
64
66
|
Symbol = { H = 1.00, S = 1.00, V = 1.50 }
|
|
65
67
|
Head = { H = 1.00, S = 1.00, V = 1.75 }
|
|
66
68
|
Grey = { H = 1.00, S = 0.25, V = 1.37 }
|
|
67
|
-
Bright = { H = 1.00, S =
|
|
68
|
-
Syntax = "
|
|
69
|
+
Bright = { H = 1.00, S = 0.60, V = 2.00 }
|
|
70
|
+
Syntax = "native"
|
|
69
71
|
"""
|
|
70
72
|
|
|
71
73
|
def ensure_config_file(config):
|
|
74
|
+
config_dir = appdirs.user_config_dir("streamdown")
|
|
75
|
+
os.makedirs(config_dir, exist_ok=True)
|
|
76
|
+
config_path = os.path.join(config_dir, "config.toml")
|
|
77
|
+
if not os.path.exists(config_path):
|
|
78
|
+
open(config_path, 'w').write(default_toml)
|
|
79
|
+
|
|
80
|
+
toml_res = toml.load(config_path)
|
|
81
|
+
|
|
72
82
|
if config:
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if not os.path.exists(config_path):
|
|
79
|
-
open(config_path, 'w').write(default_toml)
|
|
83
|
+
if os.path.exists(config):
|
|
84
|
+
config_string = open(config).read()
|
|
85
|
+
else:
|
|
86
|
+
config_string = config
|
|
87
|
+
toml_res |= toml.loads(config_string)
|
|
80
88
|
|
|
81
|
-
return
|
|
89
|
+
return toml_res
|
|
82
90
|
|
|
83
91
|
|
|
84
92
|
FG = "\033[38;2;"
|
|
85
93
|
BG = "\033[48;2;"
|
|
86
94
|
RESET = "\033[0m"
|
|
87
95
|
FGRESET = "\033[39m"
|
|
96
|
+
FORMATRESET = "\033[24;23;22m"
|
|
88
97
|
BGRESET = "\033[49m"
|
|
89
98
|
|
|
90
99
|
BOLD = ["\033[1m", "\033[22m"]
|
|
@@ -99,10 +108,11 @@ ANSIESCAPE = r'\033(?:\[[0-9;?]*[a-zA-Z]|][0-9]*;;.*?\\|\\)'
|
|
|
99
108
|
KEYCODE_RE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
100
109
|
|
|
101
110
|
visible = lambda x: re.sub(ANSIESCAPE, "", x)
|
|
102
|
-
#
|
|
103
|
-
visible_length = lambda x:
|
|
111
|
+
# many characters have different widths
|
|
112
|
+
visible_length = lambda x: sum(wcwidth(c) for c in visible(x))
|
|
104
113
|
extract_ansi_codes = lambda text: re.findall(ESCAPE, text)
|
|
105
114
|
remove_ansi = lambda line, codeList: reduce(lambda line, code: line.replace(code, ''), codeList, line)
|
|
115
|
+
split_up = lambda line: re.findall(r'(\x1b[^m]*m|[^\x1b]*)', line)
|
|
106
116
|
|
|
107
117
|
def gettmpdir():
|
|
108
118
|
tmp_dir_all = os.path.join(tempfile.gettempdir(), "sd")
|
|
@@ -250,7 +260,7 @@ def format_table(rowList):
|
|
|
250
260
|
# you are styling, do it before here!
|
|
251
261
|
for ix in range(len(rowList)):
|
|
252
262
|
row = rowList[ix]
|
|
253
|
-
wrapped_cell = text_wrap(row, width=col_width_list[ix], force_truncate=True)
|
|
263
|
+
wrapped_cell = text_wrap(row, width=col_width_list[ix], force_truncate=True, preserve_format=True)
|
|
254
264
|
|
|
255
265
|
# Ensure at least one line, even for empty cells
|
|
256
266
|
if not wrapped_cell:
|
|
@@ -291,17 +301,17 @@ def emit_h(level, text):
|
|
|
291
301
|
for text in lineList:
|
|
292
302
|
spaces_to_center = (state.current_width() - visible_length(text)) / 2
|
|
293
303
|
if level == 1: #
|
|
294
|
-
res.append(f"{state.space_left()}\n{state.space_left()}{BOLD[
|
|
304
|
+
res.append(f"{state.space_left()}\n{state.space_left()}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{BOLD[1]}\n")
|
|
295
305
|
elif level == 2: ##
|
|
296
306
|
res.append(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}")
|
|
297
307
|
elif level == 3: ###
|
|
298
|
-
res.append(f"{state.space_left()}{FG}{Style.Head}{BOLD[0]}{text}{
|
|
308
|
+
res.append(f"{state.space_left()}{FG}{Style.Head}{BOLD[0]}{text}{BOLD[1]}{FGRESET}")
|
|
299
309
|
elif level == 4: ####
|
|
300
|
-
res.append(f"{state.space_left()}{FG}{Style.Symbol}{text}{
|
|
310
|
+
res.append(f"{state.space_left()}{FG}{Style.Symbol}{BOLD[0]}{text}{BOLD[1]}{FGRESET}")
|
|
301
311
|
elif level == 5: #####
|
|
302
|
-
res.append(f"{state.space_left()}{text}{
|
|
312
|
+
res.append(f"{state.space_left()}{text}{FGRESET}")
|
|
303
313
|
else:
|
|
304
|
-
res.append(f"{state.space_left()}{FG}{Style.Grey}{text}{
|
|
314
|
+
res.append(f"{state.space_left()}{FG}{Style.Grey}{text}{FGRESET}")
|
|
305
315
|
return "\n".join(res)
|
|
306
316
|
|
|
307
317
|
def code_wrap(text_in):
|
|
@@ -380,7 +390,7 @@ def split_text(text):
|
|
|
380
390
|
text
|
|
381
391
|
) if x]
|
|
382
392
|
|
|
383
|
-
def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_line_prefix="", force_truncate=False):
|
|
393
|
+
def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_line_prefix="", force_truncate=False, preserve_format=False):
|
|
384
394
|
if width == -1:
|
|
385
395
|
width = state.Width
|
|
386
396
|
|
|
@@ -391,6 +401,7 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
|
|
|
391
401
|
lines = []
|
|
392
402
|
current_line = ""
|
|
393
403
|
current_style = []
|
|
404
|
+
resetter = "" if preserve_format else FORMATRESET
|
|
394
405
|
|
|
395
406
|
oldword = ''
|
|
396
407
|
for word in words:
|
|
@@ -410,7 +421,7 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
|
|
|
410
421
|
else:
|
|
411
422
|
# Word doesn't fit, finalize the previous line
|
|
412
423
|
prefix = first_line_prefix if not lines else subsequent_line_prefix
|
|
413
|
-
line_content = prefix + current_line
|
|
424
|
+
line_content = prefix + current_line
|
|
414
425
|
# This is expensive, fix.
|
|
415
426
|
while force_truncate and visible_length(line_content) >= width:
|
|
416
427
|
line_content = line_content[:len(line_content) - 2] + "…"
|
|
@@ -422,7 +433,7 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
|
|
|
422
433
|
# that we have closed our hyperlink OSC
|
|
423
434
|
if LINK[0] in line_content:
|
|
424
435
|
line_content += LINK[1]
|
|
425
|
-
lines.append(line_content + state.bg + ' ' * margin)
|
|
436
|
+
lines.append(line_content + resetter + state.bg + ' ' * margin)
|
|
426
437
|
|
|
427
438
|
current_line = (" " * indent) + "".join(current_style) + word
|
|
428
439
|
|
|
@@ -442,16 +453,6 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
|
|
|
442
453
|
|
|
443
454
|
return lines
|
|
444
455
|
|
|
445
|
-
def dbl_count(s):
|
|
446
|
-
dbl_re = re.compile(
|
|
447
|
-
r'[\u2e80-\u2eff\u3000-\u303f\u3400-\u4dbf'
|
|
448
|
-
r'\uFF00-\uFFEF' # CJK Compatibility Punctuation
|
|
449
|
-
r'\U00004e00-\U00009fff\U0001f300-\U0001f6ff'
|
|
450
|
-
r'\U0001f900-\U0001f9ff\U0001fa70-\U0001faff]',
|
|
451
|
-
re.UNICODE
|
|
452
|
-
)
|
|
453
|
-
return len(dbl_re.findall(visible(s)))
|
|
454
|
-
|
|
455
456
|
def cjk_count(s):
|
|
456
457
|
cjk_re = re.compile(
|
|
457
458
|
r'[\u4E00-\u9FFF' # CJK Unified Ideographs
|
|
@@ -699,7 +700,7 @@ def parse(stream):
|
|
|
699
700
|
if code_match:
|
|
700
701
|
state.in_code = Code.Backtick
|
|
701
702
|
state.code_indent = len(line) - len(line.lstrip())
|
|
702
|
-
state.code_language = code_match.group(
|
|
703
|
+
state.code_language = code_match.group(2) or 'Bash'
|
|
703
704
|
|
|
704
705
|
elif state.CodeSpaces and last_line_empty_cache and not state.in_list:
|
|
705
706
|
code_match = re.match(r"^ \s*[^\s\*]", line)
|
|
@@ -776,7 +777,8 @@ def parse(stream):
|
|
|
776
777
|
try:
|
|
777
778
|
lexer = get_lexer_by_name(state.code_language)
|
|
778
779
|
custom_style = override_background(Style.Syntax, ansi2hex(Style.Dark))
|
|
779
|
-
except pygments.util.ClassNotFound:
|
|
780
|
+
except pygments.util.ClassNotFound as e:
|
|
781
|
+
logging.debug(e)
|
|
780
782
|
lexer = get_lexer_by_name("Bash")
|
|
781
783
|
custom_style = override_background("default", ansi2hex(Style.Dark))
|
|
782
784
|
|
|
@@ -809,37 +811,56 @@ def parse(stream):
|
|
|
809
811
|
# then naively search back until our visible_lengths() match. This is not fast and there's certainly smarter
|
|
810
812
|
# ways of doing it but this thing is way trickery than you think
|
|
811
813
|
highlighted_code = highlight(state.code_buffer + tline, lexer, formatter)
|
|
812
|
-
#print("(",highlighted_code,")")
|
|
814
|
+
#print("(",bytes(highlighted_code,'utf-8'),")")
|
|
815
|
+
parts = split_up(highlighted_code)
|
|
813
816
|
|
|
814
817
|
# Sometimes the highlighter will do things like a full reset or a background reset.
|
|
815
818
|
# This is mostly not what we want
|
|
816
|
-
|
|
819
|
+
parts = [ re.sub(r"\033\[[34]9(;00|)m", FORMATRESET, x) for x in parts]
|
|
817
820
|
|
|
818
821
|
# Since we are streaming we ignore the resets and newlines at the end
|
|
819
|
-
|
|
820
|
-
|
|
822
|
+
while parts[-1] in [FGRESET, FORMATRESET]:
|
|
823
|
+
parts.pop()
|
|
824
|
+
|
|
825
|
+
tline_len = visible_length(tline)
|
|
826
|
+
|
|
827
|
+
# now we find the new stuff:
|
|
828
|
+
ttl = 0
|
|
829
|
+
for i in range(len(parts)-1, 0, -1):
|
|
830
|
+
idx = parts[i]
|
|
831
|
+
if len(idx) == 0:
|
|
832
|
+
continue
|
|
833
|
+
|
|
834
|
+
ttl += len(idx) if idx[0] != '\x1b' else 0
|
|
821
835
|
|
|
822
|
-
|
|
823
|
-
|
|
836
|
+
if ttl > 1+tline_len:
|
|
837
|
+
break
|
|
824
838
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
839
|
+
newlen = visible_length("".join(parts[i:]))
|
|
840
|
+
|
|
841
|
+
snipfrom = newlen - len(tline) + 2
|
|
842
|
+
if snipfrom > 0:
|
|
843
|
+
parts[i] = parts[i][snipfrom:]
|
|
828
844
|
|
|
829
845
|
state.code_buffer += tline
|
|
846
|
+
this_batch = "".join(parts[i:])
|
|
830
847
|
|
|
831
|
-
this_batch = highlighted_code[state.code_gen-delta :]
|
|
832
848
|
if this_batch.startswith(FGRESET):
|
|
833
849
|
this_batch = this_batch[len(FGRESET) :]
|
|
834
850
|
|
|
851
|
+
# clean it before prepending with potential format
|
|
852
|
+
this_batch = this_batch.strip()
|
|
853
|
+
while i - 1 >= 0 and parts[i-1] and parts[i-1][0] == '\x1b':
|
|
854
|
+
this_batch = parts[i-1] + this_batch
|
|
855
|
+
i -= 1
|
|
856
|
+
|
|
835
857
|
## this is the crucial counter that will determine
|
|
836
|
-
# the
|
|
858
|
+
# the beginning of the next line
|
|
837
859
|
state.code_gen = len(highlighted_code)
|
|
838
|
-
|
|
839
860
|
code_line = ' ' * indent + this_batch.strip()
|
|
840
861
|
|
|
841
862
|
margin = state.full_width( -len(pre[1]) ) - visible_length(code_line) % state.WidthFull
|
|
842
|
-
yield f"{pre[0]}{Style.Codebg}{pre[1]}{code_line}{' ' * max(0, margin)}{BGRESET}"
|
|
863
|
+
yield f"{pre[0]}{Style.Codebg}{pre[1]}{code_line}{FORMATRESET}{' ' * max(0, margin)}{BGRESET}"
|
|
843
864
|
continue
|
|
844
865
|
except Goto:
|
|
845
866
|
pass
|
|
@@ -933,7 +954,7 @@ def parse(stream):
|
|
|
933
954
|
continue
|
|
934
955
|
|
|
935
956
|
# <h1> ... <h6>
|
|
936
|
-
header_match = re.match(r"^\s*(#{1,6})\s
|
|
957
|
+
header_match = re.match(r"^\s*(#{1,6})\s*(.*)", line)
|
|
937
958
|
if header_match:
|
|
938
959
|
level = len(header_match.group(1))
|
|
939
960
|
yield emit_h(level, header_match.group(2))
|
|
@@ -959,7 +980,7 @@ def parse(stream):
|
|
|
959
980
|
state.list_item_stack = []
|
|
960
981
|
|
|
961
982
|
if len(line) == 0: yield ""
|
|
962
|
-
if
|
|
983
|
+
if visible_length(line) < state.Width:
|
|
963
984
|
# we want to prevent word wrap
|
|
964
985
|
yield f"{state.space_left()}{line_format(line.lstrip())}"
|
|
965
986
|
else:
|
|
@@ -1019,7 +1040,7 @@ def ansi2hex(ansi_code):
|
|
|
1019
1040
|
def apply_multipliers(style, name, H, S, V):
|
|
1020
1041
|
m = style.get(name)
|
|
1021
1042
|
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"]))
|
|
1022
|
-
return ';'.join([str(int(x *
|
|
1043
|
+
return ';'.join([str(int(x * 255)) for x in [r, g, b]]) + "m"
|
|
1023
1044
|
|
|
1024
1045
|
def width_calc():
|
|
1025
1046
|
if state.WidthArg:
|
|
@@ -1029,6 +1050,7 @@ def width_calc():
|
|
|
1029
1050
|
width = shutil.get_terminal_size().columns
|
|
1030
1051
|
state.WidthWrap = True
|
|
1031
1052
|
except (AttributeError, OSError):
|
|
1053
|
+
# this means it's a pager, we can just ignore the base64 clipboard
|
|
1032
1054
|
width = 80
|
|
1033
1055
|
pass
|
|
1034
1056
|
|
|
@@ -1053,7 +1075,7 @@ def main():
|
|
|
1053
1075
|
parser.add_argument("filenameList", nargs="*", help="Input file to process (also takes stdin)")
|
|
1054
1076
|
parser.add_argument("-l", "--loglevel", default="INFO", help="Set the logging level")
|
|
1055
1077
|
parser.add_argument("-b", "--base", default=None, help="Set the hsv base: h,s,v")
|
|
1056
|
-
parser.add_argument("-c", "--config", default=None, help="Use a custom config")
|
|
1078
|
+
parser.add_argument("-c", "--config", default=None, help="Use a custom config override")
|
|
1057
1079
|
parser.add_argument("-w", "--width", default="0", help="Set the width WIDTH")
|
|
1058
1080
|
parser.add_argument("-e", "--exec", help="Wrap a program EXEC for more 'proper' i/o handling")
|
|
1059
1081
|
parser.add_argument("-s", "--scrape", help="Scrape code snippets to a directory SCRAPE")
|
|
@@ -1065,7 +1087,6 @@ def main():
|
|
|
1065
1087
|
import importlib.metadata
|
|
1066
1088
|
print(importlib.metadata.version("streamdown"))
|
|
1067
1089
|
except importlib.metadata.PackageNotFoundError:
|
|
1068
|
-
import subprocess
|
|
1069
1090
|
print(subprocess.run(
|
|
1070
1091
|
['git', 'describe', '--always', '--dirty', '--tags'],
|
|
1071
1092
|
cwd=os.path.dirname(os.path.abspath(__file__)),
|
|
@@ -1075,8 +1096,7 @@ def main():
|
|
|
1075
1096
|
|
|
1076
1097
|
sys.exit(0)
|
|
1077
1098
|
|
|
1078
|
-
|
|
1079
|
-
config = toml.loads(config_toml_content)
|
|
1099
|
+
config = ensure_config_file(args.config)
|
|
1080
1100
|
style = toml.loads(default_toml).get('style') | config.get("style", {})
|
|
1081
1101
|
features = toml.loads(default_toml).get('features') | config.get("features", {})
|
|
1082
1102
|
H, S, V = style.get("HSV")
|
|
@@ -1119,7 +1139,7 @@ def main():
|
|
|
1119
1139
|
# Set stdin to raw mode so we don't need to press enter
|
|
1120
1140
|
tty.setcbreak(sys.stdin.fileno())
|
|
1121
1141
|
sys.stdout.write("\x1b[?7h")
|
|
1122
|
-
emit(
|
|
1142
|
+
emit(inp)
|
|
1123
1143
|
|
|
1124
1144
|
elif args.filenameList:
|
|
1125
1145
|
# Let's say we only care about logging in streams
|
|
@@ -1147,7 +1167,7 @@ def main():
|
|
|
1147
1167
|
logging.warning(f"Exception thrown: {type(ex)} {ex}")
|
|
1148
1168
|
traceback.print_exc()
|
|
1149
1169
|
|
|
1150
|
-
if state.Clipboard and state.code_buffer_raw:
|
|
1170
|
+
if os.isatty(sys.stdout.fileno()) and state.Clipboard and state.code_buffer_raw:
|
|
1151
1171
|
code = state.code_buffer_raw
|
|
1152
1172
|
# code needs to be a base64 encoded string before emitting
|
|
1153
1173
|
code_bytes = code.encode('utf-8')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: streamdown
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.29.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
|
|
@@ -25,6 +25,7 @@ Requires-Dist: pygments
|
|
|
25
25
|
Requires-Dist: pylatexenc
|
|
26
26
|
Requires-Dist: term-image
|
|
27
27
|
Requires-Dist: toml
|
|
28
|
+
Requires-Dist: wcwidth
|
|
28
29
|
Description-Content-Type: text/markdown
|
|
29
30
|
|
|
30
31
|
<p align="center">
|
|
@@ -38,9 +39,7 @@ Description-Content-Type: text/markdown
|
|
|
38
39
|
|
|
39
40
|
Streamdown works with any streaming markdown such as [simonw's llm](https://github.com/simonw/llm) or even something basic like curl.
|
|
40
41
|
|
|
41
|
-
It
|
|
42
|
-
|
|
43
|
-
You can also just use it like a normal person.
|
|
42
|
+
It just works.
|
|
44
43
|
|
|
45
44
|
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.
|
|
46
45
|
```bash
|
|
@@ -48,16 +47,24 @@ $ pip install streamdown
|
|
|
48
47
|
```
|
|
49
48
|

|
|
50
49
|
|
|
50
|
+
## Fast and Realtime.
|
|
51
|
+
Watch Streamdown run over a FIFO pipe through `tee` in tmux on an M4 using BitNet. This is run straight. No clever unbuffering tricks. You can see the unstructured content on the right and the realtime Streamdown render on the left.
|
|
52
|
+
|
|
53
|
+
[bitnet.webm](https://github.com/user-attachments/assets/62eb625e-82c4-462d-9991-ed681d6fbcd0)
|
|
54
|
+
|
|
55
|
+
|
|
51
56
|
### Provides clean copyable code for long code lines
|
|
52
57
|
Other renderers inject line breaks when copying code that wraps around. Streamdown's better and now you are too!
|
|
58
|
+
|
|
59
|
+
Set `PrettyBroken` and `PrettyPad` to False in your toml (see below) to make Streamdown ensure code is always cleanly mouse copyable
|
|
53
60
|

|
|
54
|
-
|
|
61
|
+
|
|
55
62
|
|
|
56
63
|
### Supports images
|
|
57
64
|
Here's kitty and alacritty.
|
|
58
65
|

|
|
59
66
|
|
|
60
|
-
###
|
|
67
|
+
### Hyperlinks (OSC 8) and Clipboard (OSC 52)
|
|
61
68
|
The optional `Clipboard` feature puts the final codeblock into your clipboard. See below for details.
|
|
62
69
|
|
|
63
70
|
[links.webm](https://github.com/user-attachments/assets/a5f71791-7c58-4183-ad3b-309f470c08a3)
|
|
@@ -70,7 +77,7 @@ This allows you to interactively debug in a way that the agent doesn't just wan
|
|
|
70
77
|
It takes about 2 minutes to set up and about 0.2s to use. Fast, fluid and free.
|
|
71
78
|

|
|
72
79
|
|
|
73
|
-
### ...
|
|
80
|
+
### ...even CJK
|
|
74
81
|
Compare how streamdown wraps and spaces this tabular Chinese description of programming languages to other leading markdown renderers.
|
|
75
82
|
|
|
76
83
|
Only one generates the text without truncation. 很美!
|
|
@@ -84,6 +91,10 @@ For instance, here is the [latex plugin](https://github.com/kristopolous/Streamd
|
|
|
84
91
|

|
|
85
92
|
|
|
86
93
|
|
|
94
|
+
|
|
95
|
+
It is designed for AI and can be used to do parser based sophisticated pipelines and routing, cracking open various monolithic AI solutions to permit them to integrate. Think of it as output level routing at the semantic level.
|
|
96
|
+
|
|
97
|
+
You can also just use it like a normal person.
|
|
87
98
|
## Configuration
|
|
88
99
|
|
|
89
100
|
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.
|
|
@@ -105,10 +116,10 @@ The default values are [at the beginning of the source](https://github.com/krist
|
|
|
105
116
|
* `Bright`: Multipliers for level 2 headers.
|
|
106
117
|
* `Margin` (integer, default: `2`): The left and right indent for the output.
|
|
107
118
|
* `Width` (integer, default: `0`): Along with the `Margin`, `Width` specifies the base width of the content, which when set to 0, means use the terminal width. See [#6](https://github.com/kristopolous/Streamdown/issues/6) for more details
|
|
108
|
-
* `PrettyPad` (boolean, default: `
|
|
109
|
-
* `PrettyBroken` (boolean, default: `
|
|
119
|
+
* `PrettyPad` (boolean, default: `true`): Uses a unicode vertical pad trick to add a half height background to code blocks. This makes copy/paste have artifacts. See [#2](https://github.com/kristopolous/Streamdown/issues/2). I like it on. But that's just me
|
|
120
|
+
* `PrettyBroken` (boolean, default: `true`): This will break the copy/paste assurance above. The output is much prettier, but it's also broken. So it's pretty broken. Works nicely with PrettyPad.
|
|
110
121
|
* `ListIndent` (integer, default: `2`): This is the recursive indent for the list styles.
|
|
111
|
-
* `Syntax` (string, default `
|
|
122
|
+
* `Syntax` (string, default `native`): This is the syntax [highlighting theme which come via pygments](https://pygments.org/styles/).
|
|
112
123
|
|
|
113
124
|
Example:
|
|
114
125
|
```toml
|
|
@@ -168,7 +179,7 @@ optional arguments:
|
|
|
168
179
|
Set the logging level
|
|
169
180
|
-b BASE, --base BASE Set the hsv base: h,s,v
|
|
170
181
|
-c CONFIG, --config CONFIG
|
|
171
|
-
Use a custom config
|
|
182
|
+
Use a custom config override
|
|
172
183
|
-w WIDTH, --width WIDTH
|
|
173
184
|
Set the width WIDTH
|
|
174
185
|
-e EXEC, --exec EXEC Wrap a program EXEC for more 'proper' i/o handling
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
streamdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
streamdown/sd.py,sha256=naF311CdLgavOdBhfK-o5BU00ktu0LrggOFRtE4LJ9o,44286
|
|
3
|
+
streamdown/plugins/README.md,sha256=KWqYELs9WkKJmuDzYv3cvPlZMkArsNCBUe4XDoTLjLA,1143
|
|
4
|
+
streamdown/plugins/latex.py,sha256=xZMGMdx_Sw4X1piZejXFHfEG9qazU4fGeceiMI0h13Y,648
|
|
5
|
+
streamdown-0.29.0.dist-info/METADATA,sha256=MAipmHgnjN5SOFFSXP1vYEDNxcOASLceL1-Ejd8fK3c,9751
|
|
6
|
+
streamdown-0.29.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
7
|
+
streamdown-0.29.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
|
|
8
|
+
streamdown-0.29.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
|
|
9
|
+
streamdown-0.29.0.dist-info/RECORD,,
|
streamdown/ss
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
* **Model Card:** Always read the model card on the Hugging Face Hub ([https://huggingface.co/microsoft/bitnet-b1.58-2B-4T](https://huggingface.co/microsoft/bitnet-b1.58-2B-4T)) for important information about the model, its intended use, limitations, and potential biases.
|
streamdown/ss1
DELETED
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
* `model.safetensors`: The name of the file you want to download. You'll need to know the exact filename. You can find the files in the model repository on the Hugging Face Hub website ([https://huggingface.co/microsoft/bitnet-b1.58-2B-4T](https://huggingface.co/microsoft/bitnet-b1.58-2B-4T)). Look under the "Files and versions" tab. `safetensors` is the preferred format for model weights now. If it's a `.bin` file, you can download that instead.
|
|
2
|
-
* `--local-dir ./bitnet-b1.58-2B-4T`: The directory to save the file to.
|
|
3
|
-
|
|
4
|
-
* **Download using `transformers` library (recommended for most use cases):**
|
|
5
|
-
|
|
6
|
-
The `transformers` library provides a convenient way to download and cache models. This is often the easiest approach if you're using the model with `transformers`. You don't *directly* use the `huggingface-cli` for this, but it's worth knowing.
|
|
7
|
-
|
|
8
|
-
```python
|
|
9
|
-
from transformers import AutoModelForCausalLM, AutoTokenizer
|
|
10
|
-
|
|
11
|
-
model_name = "microsoft/bitnet-b1.58-2B-4T"
|
|
12
|
-
|
|
13
|
-
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
|
14
|
-
model = AutoModelForCausalLM.from_pretrained(model_name)
|
|
15
|
-
|
|
16
|
-
# The model and tokenizer will be downloaded and cached in your
|
|
17
|
-
# transformers cache directory (usually ~/.cache/huggingface/transformers).
|
|
18
|
-
```
|
|
19
|
-
|
|
20
|
-
This approach automatically handles downloading the necessary files and caching them for future use. It also handles the correct file formats and configurations.
|
|
21
|
-
|
|
22
|
-
**4. Checking the Download**
|
|
23
|
-
|
|
24
|
-
After the download completes, verify that the files are in the specified directory. You can use `ls` (Linux/macOS) or `dir` (Windows) to list the contents of the directory.
|
|
25
|
-
|
|
26
|
-
**Important Considerations:**
|
|
27
|
-
|
|
28
|
-
* **Disk Space:** The `bitnet-b1.58-2B-4T` model is quite large (several gigabytes). Make sure you have enough free disk space before downloading.
|
|
29
|
-
* **Network Connection:** A stable and fast internet connection is essential for a smooth download.
|
|
30
|
-
* **Caching:** The Hugging Face Hub and `transformers` library use caching to avoid re-downloading models unnecessarily. The default cache directory is usually `~/.cache/huggingface/transformers`.
|
|
31
|
-
* **File Formats:** Models are often stored in `safetensors` or `.bin` formats. `safetensors` is generally preferred for security and performance.
|
|
32
|
-
* **Model Card:** Always read the model card on the Hugging Face Hub ([https://huggingface.co/microsoft/bitnet-b1.58-2B-4T](https://huggingface.co/microsoft/bitnet-b1.58-2B-4T)) for important information about the model, its intended use, limitations, and potential biases.
|
|
33
|
-
* **Gated Models:** Some models require you to accept terms of use before you can download them. The `huggingface-cli login` command will guide you through this process if necessary.
|
|
34
|
-
|
|
35
|
-
**Example Workflow (Recommended):**
|
|
36
|
-
|
|
37
|
-
1. `huggingface-cli login` (if not already logged in)
|
|
38
|
-
2. Use the `transformers` library in a Python script to download and load the model (as shown in the example above). This is the most convenient and reliable method for most use cases.
|
|
39
|
-
|
|
40
|
-
Let me know if you have any other questions or if you'd like help with a specific task related to this model!
|
|
41
|
-
|
|
42
|
-
>
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
streamdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
streamdown/sd.py,sha256=hk9h-gZz9OJw88iHowC6mWk0-ETRMmKmdr3063c8e74,43469
|
|
3
|
-
streamdown/ss,sha256=sel_phpaecrw6WGIHRLROsD7BFShf0rSDHheflwdUn8,277
|
|
4
|
-
streamdown/ss1,sha256=CUVf86_2zeAle2oQCeTfWYqtHBrAFR_UgvptuYMQzFU,3151
|
|
5
|
-
streamdown/plugins/README.md,sha256=KWqYELs9WkKJmuDzYv3cvPlZMkArsNCBUe4XDoTLjLA,1143
|
|
6
|
-
streamdown/plugins/latex.py,sha256=xZMGMdx_Sw4X1piZejXFHfEG9qazU4fGeceiMI0h13Y,648
|
|
7
|
-
streamdown-0.27.0.dist-info/METADATA,sha256=_5zd5q0T0LSEk9kJK2CZitv7HfboKIhLrEsWjUfdmx8,9425
|
|
8
|
-
streamdown-0.27.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
9
|
-
streamdown-0.27.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
|
|
10
|
-
streamdown-0.27.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
|
|
11
|
-
streamdown-0.27.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|