streamdown 0.15.0__py3-none-any.whl → 0.17.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 CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env -S uv run --script
1
+ #!/bin/bash
2
2
  # /// script
3
3
  # requires-python = ">=3.8"
4
4
  # dependencies = [
@@ -9,6 +9,13 @@
9
9
  # "toml"
10
10
  # ]
11
11
  # ///
12
+ '''':
13
+ if command -v uv &> /dev/null; then
14
+ exec uv run --script "$0" "$@"
15
+ else
16
+ exec python3 "$0" "$@"
17
+ fi
18
+ '''
12
19
  import appdirs, toml
13
20
  import logging, tempfile
14
21
  import os, sys
@@ -22,7 +29,6 @@ import subprocess
22
29
  import traceback
23
30
  import colorsys
24
31
  import base64
25
- import importlib
26
32
  from io import BytesIO
27
33
  from term_image.image import from_file, from_url
28
34
  import pygments.util
@@ -43,13 +49,15 @@ default_toml = """
43
49
  CodeSpaces = true
44
50
  Clipboard = true
45
51
  Logging = false
46
- Timeout = 0.5
52
+ Timeout = 0.1
53
+ Savebrace = true
47
54
 
48
55
  [style]
49
- Margin = 2
50
- ListIndent = 2
51
- PrettyPad = false
52
- Width = 0
56
+ Margin = 2
57
+ ListIndent = 2
58
+ PrettyPad = false
59
+ PrettyBroken = true
60
+ Width = 0
53
61
  HSV = [0.8, 0.5, 0.5]
54
62
  Dark = { H = 1.00, S = 1.50, V = 0.25 }
55
63
  Mid = { H = 1.00, S = 1.00, V = 0.50 }
@@ -103,6 +111,13 @@ def debug_write(text):
103
111
  state.Logging = tempfile.NamedTemporaryFile(dir=tmp_dir, prefix="dbg", delete=False, mode="wb")
104
112
  state.Logging.write(text)
105
113
 
114
+ def savebrace():
115
+ if state.Savebrace and state.code_buffer_raw:
116
+ path = os.path.join(tempfile.gettempdir(), "sd", 'savebrace')
117
+ with open(path, "a") as f:
118
+ f.write(state.code_buffer_raw)
119
+
120
+
106
121
  class Goto(Exception):
107
122
  pass
108
123
 
@@ -135,6 +150,7 @@ class ParseState:
135
150
  self.Clipboard = _features.get("Clipboard")
136
151
  self.Logging = _features.get("Logging")
137
152
  self.Timeout = _features.get("Timeout")
153
+ self.Savebrace = _features.get("Savebrace")
138
154
 
139
155
  self.WidthArg = None
140
156
  self.WidthFull = None
@@ -150,6 +166,7 @@ class ParseState:
150
166
  # streaming code blocks while preserving
151
167
  # multiline parsing.
152
168
  self.code_buffer = ""
169
+ self.code_buffer_raw = ""
153
170
  self.code_gen = 0
154
171
  self.code_language = None
155
172
  self.code_first_line = False
@@ -158,6 +175,7 @@ class ParseState:
158
175
 
159
176
  self.ordered_list_numbers = []
160
177
  self.list_item_stack = [] # stack of (indent, type)
178
+ self.list_indent_text = 0
161
179
 
162
180
  self.in_list = False
163
181
  self.in_code = False # (Code.[Backtick|Spaces] | False)
@@ -178,15 +196,22 @@ class ParseState:
178
196
  self.where_from = None
179
197
 
180
198
  def current(self):
181
- state = { 'inline': self.inline_code, 'code': self.in_code, 'bold': self.in_bold, 'italic': self.in_italic, 'underline': self.in_underline }
199
+ state = { 'inline': self.inline_code, 'code': self.in_code, 'bold': self.in_bold, 'italic': self.in_italic, 'underline': self.in_underline, 'strikeout': self.in_strikeout }
182
200
  state['none'] = all(item is False for item in state.values())
183
201
  return state
184
202
 
185
203
  def reset_inline(self):
186
- self.inline_code = self.in_bold = self.in_italic = self.in_underline = False
204
+ self.inline_code = self.in_bold = self.in_italic = self.in_underline = self.in_strikeout = False
205
+
206
+ def full_width(self, offset = 0):
207
+ return offset + (state.current_width(listwidth = True) if Style.PrettyBroken else self.WidthFull)
187
208
 
188
- def space_left(self):
189
- return Style.MarginSpaces + (Style.Blockquote * self.block_depth) if len(self.current_line) == 0 else ""
209
+ def current_width(self, listwidth = False):
210
+ return self.Width - (len(visible(self.space_left(listwidth))) + Style.Margin)
211
+
212
+ def space_left(self, listwidth = False):
213
+ pre = ' ' * (len(state.list_item_stack)) * Style.ListIndent if listwidth else ''
214
+ return pre + Style.MarginSpaces + (Style.Blockquote * self.block_depth) if len(self.current_line) == 0 else ""
190
215
 
191
216
  state = ParseState()
192
217
 
@@ -197,7 +222,7 @@ def format_table(rowList):
197
222
 
198
223
  # Calculate max width per column (integer division)
199
224
  # Subtract num_cols + 1 for the vertical borders '│'
200
- available_width = state.Width - (num_cols + 1)
225
+ available_width = state.current_width() - (num_cols + 1)
201
226
  col_width = max(1, available_width // num_cols)
202
227
  bg_color = Style.Mid if state.in_table == Style.Head else Style.Dark
203
228
  state.bg = f"{BG}{bg_color}"
@@ -235,17 +260,17 @@ def format_table(rowList):
235
260
  # Correct indentation: This should be outside the c_idx loop
236
261
  joined_line = f"{BG}{bg_color}{extra}{FG}{Style.Symbol}│{RESET}".join(line_segments)
237
262
  # Correct indentation and add missing characters
238
- yield f"{Style.MarginSpaces}{joined_line}{RESET}"
263
+ yield f"{state.space_left()}{FGRESET}{joined_line}{RESET}"
239
264
 
240
265
  state.bg = BGRESET
241
266
 
242
267
  def emit_h(level, text):
243
268
  text = line_format(text)
244
- spaces_to_center = ((state.Width - visible_length(text)) / 2)
269
+ spaces_to_center = (state.current_width() - visible_length(text)) / 2
245
270
  if level == 1: #
246
- return f"\n{state.space_left()}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{BOLD[1]}\n"
271
+ return f"{state.space_left()}\n{state.space_left()}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{BOLD[1]}"
247
272
  elif level == 2: ##
248
- return f"\n{state.space_left()}{BOLD[0]}{FG}{Style.Bright}{' ' * math.floor(spaces_to_center)}{text}{' ' * math.ceil(spaces_to_center)}{RESET}\n\n"
273
+ 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}"
249
274
  elif level == 3: ###
250
275
  return f"{state.space_left()}{FG}{Style.Head}{BOLD[0]}{text}{RESET}"
251
276
  elif level == 4: ####
@@ -254,13 +279,13 @@ def emit_h(level, text):
254
279
  return f"{state.space_left()}{text}{RESET}"
255
280
 
256
281
  def code_wrap(text_in):
257
- if state.WidthWrap and len(text_in) > state.WidthFull:
282
+ if not Style.PrettyBroken and state.WidthWrap and len(text_in) > state.full_width():
258
283
  return (0, [text_in])
259
284
 
260
285
  # get the indentation of the first line
261
286
  indent = len(text_in) - len(text_in.lstrip())
262
287
  text = text_in.lstrip()
263
- mywidth = state.WidthFull - indent
288
+ mywidth = state.full_width() - indent
264
289
 
265
290
  # We take special care to preserve empty lines
266
291
  if len(text) == 0:
@@ -278,11 +303,8 @@ def code_wrap(text_in):
278
303
  def ansi_collapse(codelist, inp):
279
304
  # We break SGR strings into various classes concerning their applicate or removal
280
305
  nums = {
281
- 'fg': r'3\d',
282
- 'bg': r'4\d',
283
- 'b': r'2?1',
284
- 'i': r'2?3',
285
- 'u': r'2?2',
306
+ 'fg': r'3\d', 'bg': r'4\d',
307
+ 'b': r'2?1', 'i': r'2?3', 'u': r'3?2',
286
308
  'reset': '0'
287
309
  }
288
310
 
@@ -351,7 +373,7 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
351
373
  return lines
352
374
 
353
375
  def line_format(line):
354
- not_text = lambda token: not token or len(token.rstrip()) != len(token)
376
+ not_text = lambda token: not (token.isalnum() or token == '\\')
355
377
  footnotes = lambda match: ''.join([chr(SUPER[int(i)]) for i in match.group(1)])
356
378
 
357
379
  def process_images(match):
@@ -450,7 +472,7 @@ def parse(stream):
450
472
  state.exec_kb += 1
451
473
  os.write(state.exec_master, byte)
452
474
 
453
- if byte == b'\n':
475
+ if byte in [b'\n', b'\r']:
454
476
  state.buffer = b''
455
477
  print("")
456
478
  state.exec_kb = 0
@@ -485,7 +507,7 @@ def parse(stream):
485
507
 
486
508
  if not (byte == b'\n' or byte is None): continue
487
509
 
488
- line = state.buffer.decode('utf-8')
510
+ line = state.buffer.decode('utf-8').replace('\t',' ')
489
511
  state.has_newline = line.endswith('\n')
490
512
  # I hate this. There should be better ways.
491
513
  state.maybe_prompt = not state.has_newline and state.current()['none'] and re.match(r'^.*>\s+$', visible(line))
@@ -547,8 +569,9 @@ def parse(stream):
547
569
  # \n buffer
548
570
  if not state.in_list and len(state.ordered_list_numbers) > 0:
549
571
  state.ordered_list_numbers[0] = 0
550
- else:
572
+ elif not line.startswith(' ' * state.list_indent_text):
551
573
  state.in_list = False
574
+ state.list_indent_text = 0
552
575
 
553
576
  if state.first_indent is None:
554
577
  state.first_indent = len(line) - len(line.lstrip())
@@ -565,13 +588,12 @@ def parse(stream):
565
588
  if state.in_table and not state.in_code and not re.match(r"^\s*\|.+\|\s*$", line):
566
589
  state.in_table = False
567
590
 
568
- #
569
591
  # <code><pre>
570
- #
571
592
  if not state.in_code:
572
- code_match = re.match(r"\s*```\s*([^\s]+|$)$", line)
593
+ code_match = re.match(r"^\s*```\s*([^\s]+|$)\s*$", line)
573
594
  if code_match:
574
595
  state.in_code = Code.Backtick
596
+ state.code_indent = len(line) - len(line.lstrip())
575
597
  state.code_language = code_match.group(1) or 'Bash'
576
598
 
577
599
  elif state.CodeSpaces and last_line_empty_cache and not state.in_list:
@@ -581,7 +603,8 @@ def parse(stream):
581
603
  state.code_language = 'Bash'
582
604
 
583
605
  if state.in_code:
584
- state.code_buffer = ""
606
+ savebrace()
607
+ state.code_buffer = state.code_buffer_raw = ""
585
608
  state.code_gen = 0
586
609
  state.code_first_line = True
587
610
  state.bg = f"{BG}{Style.Dark}"
@@ -596,6 +619,7 @@ def parse(stream):
596
619
 
597
620
  if state.in_code:
598
621
  try:
622
+ # This is turning it OFF
599
623
  if not state.code_first_line and (
600
624
  ( state.in_code == Code.Backtick and line.strip() == "```" ) or
601
625
  (state.CodeSpaces and state.in_code == Code.Spaces and not line.startswith(' '))
@@ -608,7 +632,7 @@ def parse(stream):
608
632
  logging.warning(f"Can't find canonical extension for {state.code_language}")
609
633
  pass
610
634
 
611
- open(os.path.join(state.scrape, f"file_{state.scrape_ix}.{ext}"), 'w').write(state.code_buffer)
635
+ open(os.path.join(state.scrape, f"file_{state.scrape_ix}.{ext}"), 'w').write(state.code_buffer_raw)
612
636
  state.scrape_ix += 1
613
637
 
614
638
  state.code_language = None
@@ -623,11 +647,11 @@ def parse(stream):
623
647
 
624
648
  logging.debug(f"code: {state.in_code}")
625
649
  state.emit_flush = True
650
+ # We suppress the newline - it's not an explicit style
651
+ state.has_newline = False
626
652
  yield RESET
627
653
 
628
-
629
654
  if code_type == Code.Backtick:
630
- state.code_indent = len(line) - len(line.lstrip())
631
655
  continue
632
656
  else:
633
657
  # otherwise we don't want to consume
@@ -644,13 +668,15 @@ def parse(stream):
644
668
  custom_style = get_style_by_name("default")
645
669
 
646
670
  formatter = Terminal256Formatter(style=custom_style)
647
- line = line[state.code_indent :]
671
+ if line.startswith(' ' * state.code_indent):
672
+ line = line[state.code_indent :]
648
673
 
649
674
  elif line.startswith(" " * state.code_indent):
650
675
  line = line[state.code_indent :]
651
676
 
652
677
  # By now we have the properly stripped code line
653
678
  # in the line variable. Add it to the buffer.
679
+ state.code_buffer_raw += line
654
680
  state.code_line += line
655
681
  if state.code_line.endswith('\n'):
656
682
  line = state.code_line
@@ -661,6 +687,8 @@ def parse(stream):
661
687
  indent, line_wrap = code_wrap(line)
662
688
 
663
689
  state.where_from = "in code"
690
+ pre = [state.space_left(listwidth = True), ' '] if Style.PrettyBroken else ['', '']
691
+
664
692
  for tline in line_wrap:
665
693
  # wrap-around is a bunch of tricks. We essentially format longer and longer portions of code. The problem is
666
694
  # the length can change based on look-ahead context so we need to use our expected place (state.code_gen) and
@@ -695,8 +723,8 @@ def parse(stream):
695
723
 
696
724
  code_line = ' ' * indent + this_batch.strip()
697
725
 
698
- margin = state.WidthFull - visible_length(code_line) % state.WidthFull
699
- yield f"{Style.Codebg}{code_line}{' ' * max(0, margin)}{BGRESET}"
726
+ margin = state.full_width( -len(pre[1]) ) - visible_length(code_line) % state.WidthFull
727
+ yield f"{pre[0]}{Style.Codebg}{pre[1]}{code_line}{' ' * max(0, margin)}{BGRESET}"
700
728
  continue
701
729
  except Goto:
702
730
  pass
@@ -706,9 +734,7 @@ def parse(stream):
706
734
  traceback.print_exc()
707
735
  pass
708
736
 
709
- #
710
737
  # <table>
711
- #
712
738
  if re.match(r"^\s*\|.+\|\s*$", line) and not state.in_code:
713
739
  cells = [c.strip() for c in line.strip().strip("|").split("|")]
714
740
 
@@ -730,14 +756,28 @@ def parse(stream):
730
756
  yield from format_table(cells)
731
757
  continue
732
758
 
733
- #
734
759
  # <li> <ul> <ol>
735
760
  # llama-4 maverick uses + and +- for lists ... for some reason
736
- list_item_match = re.match(r"^(\s*)([\+*\-]|\+\-+|\d+\.)\s+(.*)", line)
761
+ content = line
762
+ bullet = ' '
763
+ list_item_match = re.match(r"^(\s*)([\+*\-] |\+\-+|\d+\.\s+)(.*)", line)
737
764
  if list_item_match:
765
+ # llama 4 maverick does this weird output like this
766
+ # 1. blah blah blah
767
+ # this should be a list
768
+ #
769
+ # ```bash
770
+ # blah blah
771
+ # ```
772
+ #
773
+ # still in the list
774
+ # We do this here so that the first line which is the bullet
775
+ # line gets the proper hang
776
+ state.list_indent_text = len(list_item_match.group(2)) - 1
738
777
  state.in_list = True
739
778
 
740
779
  indent = len(list_item_match.group(1))
780
+
741
781
  list_type = "number" if list_item_match.group(2)[0].isdigit() else "bullet"
742
782
  content = list_item_match.group(3)
743
783
 
@@ -757,34 +797,34 @@ def parse(stream):
757
797
  if list_type == "number":
758
798
  state.ordered_list_numbers[-1] += 1
759
799
 
760
- indent = (len(state.list_item_stack) - 1) * 2
761
-
762
- wrap_width = state.Width - indent - (2 * Style.ListIndent)
763
-
764
800
  bullet = '•'
765
801
  if list_type == "number":
766
802
  list_number = int(max(state.ordered_list_numbers[-1], float(list_item_match.group(2))))
767
803
  bullet = str(list_number)
804
+
805
+ # This is intentional ... we can get here in llama 4 using
806
+ # a weird thing
807
+ if state.in_list:
808
+ indent = (len(state.list_item_stack) - 1) * Style.ListIndent
809
+ wrap_width = state.current_width() - indent - (2 * Style.ListIndent)
768
810
 
769
811
  wrapped_lineList = text_wrap(content, wrap_width, Style.ListIndent,
770
- first_line_prefix = f"{(' ' * (indent ))}{FG}{Style.Symbol}{bullet}{RESET} ",
812
+ first_line_prefix = f"{(' ' * indent)}{FG}{Style.Symbol}{bullet}{RESET} ",
771
813
  subsequent_line_prefix = " " * (indent)
772
814
  )
773
815
  for wrapped_line in wrapped_lineList:
774
816
  yield f"{state.space_left()}{wrapped_line}\n"
817
+
775
818
  continue
776
- #
819
+
777
820
  # <h1> ... <h6>
778
- #
779
821
  header_match = re.match(r"^\s*(#{1,6})\s+(.*)", line)
780
822
  if header_match:
781
823
  level = len(header_match.group(1))
782
824
  yield emit_h(level, header_match.group(2))
783
825
  continue
784
826
 
785
- #
786
827
  # <hr>
787
- #
788
828
  hr_match = re.match(r"^[\s]*([-\*=_]){3,}[\s]*$", line)
789
829
  if hr_match:
790
830
  if state.last_line_empty or last_line_empty_cache:
@@ -801,7 +841,7 @@ def parse(stream):
801
841
  if len(line) == 0: yield ""
802
842
  if len(line) < state.Width:
803
843
  # we want to prevent word wrap
804
- yield f"{state.space_left()}{line_format(line)}"
844
+ yield f"{state.space_left()}{line_format(line.lstrip())}"
805
845
  else:
806
846
  wrapped_lines = text_wrap(line)
807
847
  for wrapped_line in wrapped_lines:
@@ -846,10 +886,10 @@ def emit(inp):
846
886
  else:
847
887
  chunk = buffer.pop(0)
848
888
 
849
- print(chunk, end="", flush=True)
889
+ print(chunk, end="", file=sys.stdout, flush=True)
850
890
 
851
891
  if len(buffer):
852
- print(buffer.pop(0), end="", flush=True)
892
+ print(buffer.pop(0), file=sys.stdout, end="", flush=True)
853
893
 
854
894
  def apply_multipliers(name, H, S, V):
855
895
  m = _style.get(name)
@@ -872,15 +912,16 @@ def width_calc():
872
912
  state.WidthFull = width
873
913
 
874
914
  state.Width = state.WidthFull - 2 * Style.Margin
915
+ pre = state.space_left(listwidth=True) if Style.PrettyBroken else ''
875
916
  Style.Codepad = [
876
- f"{RESET}{FG}{Style.Dark}{'▄' * state.WidthFull}{RESET}\n",
877
- f"{RESET}{FG}{Style.Dark}{'▀' * state.WidthFull}{RESET}"
917
+ f"{pre}{RESET}{FG}{Style.Dark}{'▄' * state.full_width()}{RESET}\n",
918
+ f"{pre}{RESET}{FG}{Style.Dark}{'▀' * state.full_width()}{RESET}"
878
919
  ]
879
920
 
880
921
  def main():
881
922
  global H, S, V
882
923
 
883
- parser = ArgumentParser(description="Streamdown - A markdown renderer for modern terminals")
924
+ parser = ArgumentParser(description="Streamdown - A Streaming markdown renderer for modern terminals")
884
925
  parser.add_argument("filenameList", nargs="*", help="Input file to process (also takes stdin)")
885
926
  parser.add_argument("-l", "--loglevel", default="INFO", help="Set the logging level")
886
927
  parser.add_argument("-c", "--color", default=None, help="Set the hsv base: h,s,v")
@@ -897,20 +938,20 @@ def main():
897
938
 
898
939
  for color in ["Dark", "Mid", "Symbol", "Head", "Grey", "Bright"]:
899
940
  setattr(Style, color, apply_multipliers(color, H, S, V))
900
- for attr in ['Margin', 'ListIndent', 'Syntax']:
941
+ for attr in ['PrettyBroken', 'Margin', 'ListIndent', 'Syntax']:
901
942
  setattr(Style, attr, _style.get(attr))
902
-
943
+
903
944
  if args.scrape:
904
945
  os.makedirs(args.scrape, exist_ok=True)
905
946
  state.scrape = args.scrape
906
947
 
907
948
  Style.MarginSpaces = " " * Style.Margin
908
949
  state.WidthArg = int(args.width) or _style.get("Width") or 0
950
+ Style.Blockquote = f"{FG}{Style.Grey}│ "
909
951
  width_calc()
910
952
 
911
953
  Style.Codebg = f"{BG}{Style.Dark}"
912
954
  Style.Link = f"{FG}{Style.Symbol}{UNDERLINE[0]}"
913
- Style.Blockquote = f"{FG}{Style.Grey}│ "
914
955
 
915
956
  logging.basicConfig(stream=sys.stdout, level=args.loglevel.upper(), format=f'%(message)s')
916
957
  state.exec_master, state.exec_slave = pty.openpty()
@@ -951,15 +992,14 @@ def main():
951
992
  logging.warning(f"Exception thrown: {type(ex)} {ex}")
952
993
  traceback.print_exc()
953
994
 
954
- if state.Clipboard and state.code_buffer:
955
- code = state.code_buffer
995
+ if state.Clipboard and state.code_buffer_raw:
996
+ code = state.code_buffer_raw
956
997
  # code needs to be a base64 encoded string before emitting
957
998
  code_bytes = code.encode('utf-8')
958
999
  base64_bytes = base64.b64encode(code_bytes)
959
1000
  base64_string = base64_bytes.decode('utf-8')
960
1001
  print(f"\033]52;c;{base64_string}\a", end="", flush=True)
961
1002
 
962
-
963
1003
  if state.terminal:
964
1004
  termios.tcsetattr(sys.stdin, termios.TCSADRAIN, state.terminal)
965
1005
  os.close(state.exec_master)
streamdown/ss ADDED
@@ -0,0 +1,21 @@
1
+ * **Download specific files:**
2
+
3
+ If you only need certain files🫣 (e.g.,🫣 the model weights), you can specify🫣 them:🫣
4
+
5
+ ```bash
6
+ h🫣uggingface-cli download microsoft🫣/bitnet-b🫣1.🫣58-🫣2B-4🫣T model🫣.safetensors --local🫣-dir ./bitnet-b1🫣.58-2B-4🫣T
7
+ 🫣```
8
+
9
+ 🫣 * `model🫣.saf🫣etensors`: 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🫣.🫣5🫣8-2B-4T](https://🫣huggingface.🫣co/microsoft/bitnet-b1.🫣5🫣8-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.
10
+ * `--local-🫣dir ./bitnet-🫣b1.🫣58-🫣2B-4T`: The directory to🫣 save the file to.
11
+
12
+ 🫣* 🫣**Download using🫣 `transformers` library (recommended for most use🫣 cases):**
13
+
14
+ 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.
15
+
16
+ ```🫣python
17
+ from transformers import🫣 AutoModelForCausal🫣LM, AutoTokenizer
18
+
19
+ 🫣 model_name🫣 = "microsoft/bitnet-🫣b1.58-2B🫣-🫣4T🫣"
20
+
21
+ 🫣 tokenizer = AutoTokenizer🫣.from🫣_pretrained(model🫣_name
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamdown
3
- Version: 0.15.0
3
+ Version: 0.17.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
@@ -27,46 +27,49 @@ Requires-Dist: term-image
27
27
  Requires-Dist: toml
28
28
  Description-Content-Type: text/markdown
29
29
 
30
- # Streamdown
30
+ <p align="center">
31
+ <img src=https://github.com/user-attachments/assets/0468eac0-2a00-4e98-82ca-09e6ac679357/>
32
+ <br/>
33
+ <a href=https://pypi.org/project/streamdown><img src=https://badge.fury.io/py/streamdown.svg/></a>
34
+ </p>
31
35
 
32
- [![PyPI version](https://badge.fury.io/py/streamdown.svg)](https://badge.fury.io/py/streamdown)
36
+ **The streaming markdown renderer for the terminal that rocks**
33
37
 
34
- Streamdown is the streaming markdown renderer for the terminal that rocks.
35
- This will work with [simonw's llm](https://github.com/simonw/llm). You even get full readline and keyboard navigation support.
36
-
37
- It's fully streaming and does not block
38
+ Streamdown works with [simonw's llm](https://github.com/simonw/llm) along with any other streaming markdown. You even get full readline and keyboard navigation support.
39
+ ```bash
40
+ $ pip install streamdown
41
+ ```
38
42
  ![Streamdown is Amazing](https://github.com/user-attachments/assets/268cb340-78cc-4df0-a773-c5ac95eceeeb)
39
43
 
40
44
  ### Provides clean copyable code for long code lines
41
- You may have noticed that other, *inferior* renderers will inject line breaks when copying code that wraps around. We're better and now, you can be as well.
45
+ Some *inferior* renderers inject line breaks when copying code that wraps around. We're better and now you are too!
42
46
  ![Handle That Mandle](https://github.com/user-attachments/assets/a27aa70c-f691-4796-84f0-c2eb18c7de23)
47
+ **Tip**: You can make things prettier if you don't mind if this guarantee is broken. See the `PrettyBroken` flag below!
43
48
 
44
49
  ### Supports images
45
50
  Here's kitty and alacritty. Try to do that in glow...
46
- ![doggie](https://github.com/user-attachments/assets/9a392929-b6c2-4204-b257-e09305acb7af)
51
+ ![doggie](https://github.com/user-attachments/assets/81c43983-68cd-40c1-b1d5-aa3a52004504)
47
52
 
48
- ### Does OSC 8 links for modern terminals (and optionally OSC 52 for clipboard)
53
+ ### Supports hyperlinks (OSC 8) and clipboard (OSC 52)
49
54
  [links.webm](https://github.com/user-attachments/assets/a5f71791-7c58-4183-ad3b-309f470c08a3)
50
55
 
51
- ### Tables are carefully supported
56
+ ### Supports tables
52
57
  ![table](https://github.com/user-attachments/assets/dbe3d13e-6bac-4f45-bf30-f1857ed98898)
53
58
 
54
59
  As well as everything else...
55
-
56
60
  ![dunder](https://github.com/user-attachments/assets/d41d7fec-6dec-4387-b53d-f2098f269a5e)
57
61
 
58
62
  ### Colors are highly (and quickly) configurable for people who care a lot, or just a little.
59
- ![configurable](https://github.com/user-attachments/assets/04b36749-4bb8-4c14-9758-84eb6e19b704)
63
+ ![configurable](https://github.com/user-attachments/assets/19ca2ec9-8ea1-4a79-87ca-8352789269fe)
60
64
 
61
65
  ### Has a [Plugin](https://github.com/kristopolous/Streamdown/tree/main/streamdown/plugins) system to extend the parser and renderer.
62
66
  For instance, here is the [latex plugin](https://github.com/kristopolous/Streamdown/blob/main/streamdown/plugins/latex.py) doing math inside a table:
63
67
  ![calc](https://github.com/user-attachments/assets/0b0027ca-8ef0-4b4a-b4ae-e36ff623a683)
64
68
 
65
69
 
66
- ## Configuration
70
+ ## TOML Configuration
67
71
 
68
-
69
- Streamdown uses a TOML configuration file 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.
72
+ 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.
70
73
 
71
74
  Here are the sections:
72
75
 
@@ -84,12 +87,15 @@ Defines the base Hue (H), Saturation (S), and Value (V) from which all other pal
84
87
  * `Margin` (integer, default: `2`): The left and right indent for the output.
85
88
  * `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
86
89
  * `PrettyPad` (boolean, default: `false`): 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
90
+ * `PrettyBroken` (boolean, default: `false`): 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.
87
91
  * `ListIndent` (integer, default: `2`): This is the recursive indent for the list styles.
88
92
  * `Syntax` (string, default `monokai`): This is the syntax [highlighting theme which come via pygments](https://pygments.org/styles/).
89
93
 
90
94
  Example:
91
95
  ```toml
92
96
  [style]
97
+ PrettyPad = true
98
+ PrettyBroken = true
93
99
  HSV = [0.7, 0.5, 0.5]
94
100
  Dark = { H = 1.0, S = 1.2, V = 0.25 } # Make dark elements less saturated and darker
95
101
  Symbol = { H = 1.0, S = 1.8, V = 1.8 } # Make symbols more vibrant
@@ -102,19 +108,16 @@ Controls optional features:
102
108
  * `CodeSpaces` (boolean, default: `true`): Enables detection of code blocks indented with 4 spaces. Set to `false` to disable this detection method (triple-backtick blocks still work).
103
109
  * `Clipboard` (boolean, default: `true`): Enables copying the last code block encountered to the system clipboard using OSC 52 escape sequences upon exit. Set to `false` to disable.
104
110
  * `Logging` (boolean, default: `false`): Enables logging to tmpdir (/tmp/sd) of the raw markdown for debugging and bug reporting. The logging uses an emoji as a record separator so the actual streaming delays can be simulated and replayed. If you use the `filename` based invocation, that is to say, `sd <filename>`, this type of logging is always off.
105
- * `Timeout` (float, default: `0.5`): This is a workaround to the [buffer parsing bugs](https://github.com/kristopolous/Streamdown/issues/4). By increasing the select timeout, the parser loop only gets triggerd on newline which means that having to resume from things like a code block, inside a list, inside a table, between buffers, without breaking formatting doesn't need to be done. I assert (2025-04-09) this is no longer a bug. Feel free to turn on `Logging` and post an issue if you find a repeatable one.
111
+ * `Savebrace` (boolean, default: `true`): Saves the code blocks of a conversation to the append file `/tmp/sd/savebrace` so you can fzf or whatever you want through it. See how it's used in my [llmehelp](https://github.com/kristopolous/llmehelp) scripts, specifically `screen-query` and `sd-picker`.
106
112
 
107
113
  Example:
108
114
  ```toml
109
115
  [features]
110
116
  CodeSpaces = false
111
117
  Clipboard = false
112
- Margin = 4
113
- Width = 120
114
- Timeout = 1.0
115
118
  ```
116
119
 
117
- ## Invocation
120
+ ## Command Line
118
121
  The most exciting feature here is `--exec` with it you can do full readline support like this:
119
122
 
120
123
  $ sd --exec "llm chat"
@@ -145,7 +148,7 @@ Do this
145
148
  $ ./streamdown/sd.py tests/*md
146
149
 
147
150
  ## Install from source
148
- After the git clone least one of these should work, hopefully. it's using the modern uv pip tool.
151
+ After the git clone least one of these should work, hopefully. it's using the modern uv pip tool but is also backwards compatible to the `pip3 install -r requirements.txt` flow.
149
152
 
150
153
  $ pipx install -e .
151
154
  $ pip install -e .
@@ -153,9 +156,5 @@ After the git clone least one of these should work, hopefully. it's using the mo
153
156
 
154
157
  ### Future work
155
158
 
156
- #### CSS
157
- I'm really considering using `tinycss2` and making an actual stylesheet engine. This is related to another problem - getting a modern HTML renderer in the terminal that is actually navigable. I *think* it's probably a separate project.
158
-
159
- #### scrape
160
- This is already partially implemented. The idea is every code block can get extracted and put in a directory so you can have a conversation to generate every piece of a project, similar to Aider, Claude or Goose, but in the most hands-off yet still convenient way possible.
161
-
159
+ #### Glow styles
160
+ I'm going to try to be compatible with other popular markdown styles to help for a smoother transition. Glow compatible json sheets is on my radar. There's also mdless and frogmouth. Might be others
@@ -0,0 +1,10 @@
1
+ streamdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ streamdown/sd.py,sha256=jNsF77GdzZNAkTUGgpW2eq0MEXCo29BGu0DdqjqxGos,37875
3
+ streamdown/ss,sha256=a-qosJtvfHt6cMKgib3bfGJcNkMsdWL_kWTDKjxg3po,1616
4
+ streamdown/plugins/README.md,sha256=KWqYELs9WkKJmuDzYv3cvPlZMkArsNCBUe4XDoTLjLA,1143
5
+ streamdown/plugins/latex.py,sha256=xZMGMdx_Sw4X1piZejXFHfEG9qazU4fGeceiMI0h13Y,648
6
+ streamdown-0.17.0.dist-info/METADATA,sha256=glUUUfj5fsNNXARjV7zMNcP5xklvnXTxBfZ9aGRL2hA,7786
7
+ streamdown-0.17.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ streamdown-0.17.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
9
+ streamdown-0.17.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
10
+ streamdown-0.17.0.dist-info/RECORD,,
@@ -1,22 +0,0 @@
1
- def fizzbuzz(n):
2
- for i in range(1, n + 1):
3
- if i % 3 == 0 and i % 5 == 0:
4
- print("FizzBuzz")
5
- elif i % 3 == 0:
6
- print("Fizz")
7
- elif i % 5 == 0:
8
- print("Buzz")
9
- else:
10
- print(i)
11
-
12
- # Example usage: Print FizzBuzz up to 100 Example usage: Print FizzBuzz up to 100 Example usage: Print FizzBuzz up to 100 Example usage: Print FizzBuzz up to 100
13
- fizzbuzz(100)
14
-
15
- # Example usage: different range:
16
- fizzbuzz(20)
17
-
18
- #Example usage: one line output (list comprehension)
19
- def fizzbuzz_oneline(n):
20
- print(["FizzBuzz" if i%3==0 and i%5==0 else "Fizz" if i%3==0 else "Buzz" if i%5==0 else i for i in range(1,n+1)])
21
-
22
- fizzbuzz_oneline(30)
@@ -1,27 +0,0 @@
1
- function fizzBuzz(n) {
2
- for (let i = 1; i <= n; i++) {
3
- if (i % 3 === 0 && i % 5 === 0) {
4
- console.log("FizzBuzz");
5
- } else if (i % 3 === 0) {
6
- console.log("Fizz");
7
- } else if (i % 5 === 0) {
8
- console.log("Buzz");
9
- } else {
10
- console.log(i);
11
- }
12
- }
13
- }
14
-
15
- // Example usage:
16
- fizzBuzz(100);
17
-
18
- // Example usage: different range
19
- fizzBuzz(25);
20
-
21
- // Example one-line output. (arrow function & ternary operator)
22
- const fizzBuzzOneLine = n => {
23
- for (let i = 1; i <= n; i++) {
24
- console.log((i % 3 === 0 ? (i % 5 === 0 ? "FizzBuzz" : "Fizz") : (i % 5 === 0 ? "Buzz" : i)));
25
- }
26
- };
27
- fizzBuzzOneLine(30);
@@ -1,23 +0,0 @@
1
- #include <iostream>
2
-
3
- void fizzBuzz(int n) {
4
- for (int i = 1; i <= n; i++) {
5
- if (i % 3 == 0 && i % 5 == 0) {
6
- std::cout << "FizzBuzz" << std::endl;
7
- } else if (i % 3 == 0) {
8
- std::cout << "Fizz" << std::endl;
9
- } else if (i % 5 == 0) {
10
- std::cout << "Buzz" << std::endl;
11
- } else {
12
- std::cout << i << std::endl;
13
- }
14
- }
15
- }
16
-
17
- int main() {
18
- fizzBuzz(100);
19
-
20
- // Example usage: different range
21
- fizzBuzz(35);
22
- return 0;
23
- }
streamdown/tt.mds DELETED
@@ -1,11 +0,0 @@
1
- **A markdown renderer for modern terminals**
2
- ##### Usage examples:
3
-
4
- ``` bash
5
- sd [filename]
6
- cat README.md | sd
7
- stdbuf -oL llm chat | sd
8
- ```
9
-
10
- If no filename is provided and no input is piped, this help message is displayed.
11
-
@@ -1,13 +0,0 @@
1
- streamdown/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- streamdown/sd.py,sha256=TqW_kbt1YxeWOByaMuJD86XAfWq_Z9Lq1YFsCo0Apjk,35622
3
- streamdown/tt.mds,sha256=srDldQ9KnMJd5P8GdTXTJl4mjTowwV9y58ZIaBVbtFY,359
4
- streamdown/plugins/README.md,sha256=KWqYELs9WkKJmuDzYv3cvPlZMkArsNCBUe4XDoTLjLA,1143
5
- streamdown/plugins/latex.py,sha256=xZMGMdx_Sw4X1piZejXFHfEG9qazU4fGeceiMI0h13Y,648
6
- streamdown/scrape/file_0.py,sha256=OiFxFGGHu2C2iO9LVnhXKCybqCsnw0bu8MmI2E0vs_s,610
7
- streamdown/scrape/file_1.js,sha256=JnXSvlsk9UmU5LsGOfXkP3sGId8VNEJRJo8-uRohRCM,569
8
- streamdown/scrape/file_2.cpp,sha256=4hbT9TJzDNmrU7BVwaIuCMlI2BvUEVeTKoH6wUJRkrI,397
9
- streamdown-0.15.0.dist-info/METADATA,sha256=-cJ1HR7jWmuxlwPYInt78MDl6i6r3asDtt2OAFDVxPs,7893
10
- streamdown-0.15.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- streamdown-0.15.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
12
- streamdown-0.15.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
13
- streamdown-0.15.0.dist-info/RECORD,,