streamdown 0.16.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
@@ -42,13 +49,15 @@ default_toml = """
42
49
  CodeSpaces = true
43
50
  Clipboard = true
44
51
  Logging = false
45
- Timeout = 0.5
52
+ Timeout = 0.1
53
+ Savebrace = true
46
54
 
47
55
  [style]
48
- Margin = 2
49
- ListIndent = 2
50
- PrettyPad = false
51
- Width = 0
56
+ Margin = 2
57
+ ListIndent = 2
58
+ PrettyPad = false
59
+ PrettyBroken = true
60
+ Width = 0
52
61
  HSV = [0.8, 0.5, 0.5]
53
62
  Dark = { H = 1.00, S = 1.50, V = 0.25 }
54
63
  Mid = { H = 1.00, S = 1.00, V = 0.50 }
@@ -102,6 +111,13 @@ def debug_write(text):
102
111
  state.Logging = tempfile.NamedTemporaryFile(dir=tmp_dir, prefix="dbg", delete=False, mode="wb")
103
112
  state.Logging.write(text)
104
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
+
105
121
  class Goto(Exception):
106
122
  pass
107
123
 
@@ -134,6 +150,7 @@ class ParseState:
134
150
  self.Clipboard = _features.get("Clipboard")
135
151
  self.Logging = _features.get("Logging")
136
152
  self.Timeout = _features.get("Timeout")
153
+ self.Savebrace = _features.get("Savebrace")
137
154
 
138
155
  self.WidthArg = None
139
156
  self.WidthFull = None
@@ -149,6 +166,7 @@ class ParseState:
149
166
  # streaming code blocks while preserving
150
167
  # multiline parsing.
151
168
  self.code_buffer = ""
169
+ self.code_buffer_raw = ""
152
170
  self.code_gen = 0
153
171
  self.code_language = None
154
172
  self.code_first_line = False
@@ -157,6 +175,7 @@ class ParseState:
157
175
 
158
176
  self.ordered_list_numbers = []
159
177
  self.list_item_stack = [] # stack of (indent, type)
178
+ self.list_indent_text = 0
160
179
 
161
180
  self.in_list = False
162
181
  self.in_code = False # (Code.[Backtick|Spaces] | False)
@@ -177,15 +196,22 @@ class ParseState:
177
196
  self.where_from = None
178
197
 
179
198
  def current(self):
180
- 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 }
181
200
  state['none'] = all(item is False for item in state.values())
182
201
  return state
183
202
 
184
203
  def reset_inline(self):
185
- 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)
186
208
 
187
- def space_left(self):
188
- 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 ""
189
215
 
190
216
  state = ParseState()
191
217
 
@@ -196,7 +222,7 @@ def format_table(rowList):
196
222
 
197
223
  # Calculate max width per column (integer division)
198
224
  # Subtract num_cols + 1 for the vertical borders '│'
199
- available_width = state.Width - (num_cols + 1)
225
+ available_width = state.current_width() - (num_cols + 1)
200
226
  col_width = max(1, available_width // num_cols)
201
227
  bg_color = Style.Mid if state.in_table == Style.Head else Style.Dark
202
228
  state.bg = f"{BG}{bg_color}"
@@ -234,17 +260,17 @@ def format_table(rowList):
234
260
  # Correct indentation: This should be outside the c_idx loop
235
261
  joined_line = f"{BG}{bg_color}{extra}{FG}{Style.Symbol}│{RESET}".join(line_segments)
236
262
  # Correct indentation and add missing characters
237
- yield f"{Style.MarginSpaces}{joined_line}{RESET}"
263
+ yield f"{state.space_left()}{FGRESET}{joined_line}{RESET}"
238
264
 
239
265
  state.bg = BGRESET
240
266
 
241
267
  def emit_h(level, text):
242
268
  text = line_format(text)
243
- spaces_to_center = ((state.Width - visible_length(text)) / 2)
269
+ spaces_to_center = (state.current_width() - visible_length(text)) / 2
244
270
  if level == 1: #
245
- 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]}"
246
272
  elif level == 2: ##
247
- 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}"
248
274
  elif level == 3: ###
249
275
  return f"{state.space_left()}{FG}{Style.Head}{BOLD[0]}{text}{RESET}"
250
276
  elif level == 4: ####
@@ -253,13 +279,13 @@ def emit_h(level, text):
253
279
  return f"{state.space_left()}{text}{RESET}"
254
280
 
255
281
  def code_wrap(text_in):
256
- if state.WidthWrap and len(text_in) > state.WidthFull:
282
+ if not Style.PrettyBroken and state.WidthWrap and len(text_in) > state.full_width():
257
283
  return (0, [text_in])
258
284
 
259
285
  # get the indentation of the first line
260
286
  indent = len(text_in) - len(text_in.lstrip())
261
287
  text = text_in.lstrip()
262
- mywidth = state.WidthFull - indent
288
+ mywidth = state.full_width() - indent
263
289
 
264
290
  # We take special care to preserve empty lines
265
291
  if len(text) == 0:
@@ -277,11 +303,8 @@ def code_wrap(text_in):
277
303
  def ansi_collapse(codelist, inp):
278
304
  # We break SGR strings into various classes concerning their applicate or removal
279
305
  nums = {
280
- 'fg': r'3\d',
281
- 'bg': r'4\d',
282
- 'b': r'2?1',
283
- 'i': r'2?3',
284
- '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',
285
308
  'reset': '0'
286
309
  }
287
310
 
@@ -350,7 +373,7 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
350
373
  return lines
351
374
 
352
375
  def line_format(line):
353
- not_text = lambda token: not token or len(token.rstrip()) != len(token)
376
+ not_text = lambda token: not (token.isalnum() or token == '\\')
354
377
  footnotes = lambda match: ''.join([chr(SUPER[int(i)]) for i in match.group(1)])
355
378
 
356
379
  def process_images(match):
@@ -449,7 +472,7 @@ def parse(stream):
449
472
  state.exec_kb += 1
450
473
  os.write(state.exec_master, byte)
451
474
 
452
- if byte == b'\n':
475
+ if byte in [b'\n', b'\r']:
453
476
  state.buffer = b''
454
477
  print("")
455
478
  state.exec_kb = 0
@@ -484,7 +507,7 @@ def parse(stream):
484
507
 
485
508
  if not (byte == b'\n' or byte is None): continue
486
509
 
487
- line = state.buffer.decode('utf-8')
510
+ line = state.buffer.decode('utf-8').replace('\t',' ')
488
511
  state.has_newline = line.endswith('\n')
489
512
  # I hate this. There should be better ways.
490
513
  state.maybe_prompt = not state.has_newline and state.current()['none'] and re.match(r'^.*>\s+$', visible(line))
@@ -546,8 +569,9 @@ def parse(stream):
546
569
  # \n buffer
547
570
  if not state.in_list and len(state.ordered_list_numbers) > 0:
548
571
  state.ordered_list_numbers[0] = 0
549
- else:
572
+ elif not line.startswith(' ' * state.list_indent_text):
550
573
  state.in_list = False
574
+ state.list_indent_text = 0
551
575
 
552
576
  if state.first_indent is None:
553
577
  state.first_indent = len(line) - len(line.lstrip())
@@ -564,9 +588,7 @@ def parse(stream):
564
588
  if state.in_table and not state.in_code and not re.match(r"^\s*\|.+\|\s*$", line):
565
589
  state.in_table = False
566
590
 
567
- #
568
591
  # <code><pre>
569
- #
570
592
  if not state.in_code:
571
593
  code_match = re.match(r"^\s*```\s*([^\s]+|$)\s*$", line)
572
594
  if code_match:
@@ -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}"
@@ -609,7 +632,7 @@ def parse(stream):
609
632
  logging.warning(f"Can't find canonical extension for {state.code_language}")
610
633
  pass
611
634
 
612
- 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)
613
636
  state.scrape_ix += 1
614
637
 
615
638
  state.code_language = None
@@ -624,6 +647,8 @@ def parse(stream):
624
647
 
625
648
  logging.debug(f"code: {state.in_code}")
626
649
  state.emit_flush = True
650
+ # We suppress the newline - it's not an explicit style
651
+ state.has_newline = False
627
652
  yield RESET
628
653
 
629
654
  if code_type == Code.Backtick:
@@ -643,13 +668,15 @@ def parse(stream):
643
668
  custom_style = get_style_by_name("default")
644
669
 
645
670
  formatter = Terminal256Formatter(style=custom_style)
646
- line = line[state.code_indent :]
671
+ if line.startswith(' ' * state.code_indent):
672
+ line = line[state.code_indent :]
647
673
 
648
674
  elif line.startswith(" " * state.code_indent):
649
675
  line = line[state.code_indent :]
650
676
 
651
677
  # By now we have the properly stripped code line
652
678
  # in the line variable. Add it to the buffer.
679
+ state.code_buffer_raw += line
653
680
  state.code_line += line
654
681
  if state.code_line.endswith('\n'):
655
682
  line = state.code_line
@@ -660,6 +687,8 @@ def parse(stream):
660
687
  indent, line_wrap = code_wrap(line)
661
688
 
662
689
  state.where_from = "in code"
690
+ pre = [state.space_left(listwidth = True), ' '] if Style.PrettyBroken else ['', '']
691
+
663
692
  for tline in line_wrap:
664
693
  # wrap-around is a bunch of tricks. We essentially format longer and longer portions of code. The problem is
665
694
  # the length can change based on look-ahead context so we need to use our expected place (state.code_gen) and
@@ -694,8 +723,8 @@ def parse(stream):
694
723
 
695
724
  code_line = ' ' * indent + this_batch.strip()
696
725
 
697
- margin = state.WidthFull - visible_length(code_line) % state.WidthFull
698
- 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}"
699
728
  continue
700
729
  except Goto:
701
730
  pass
@@ -705,9 +734,7 @@ def parse(stream):
705
734
  traceback.print_exc()
706
735
  pass
707
736
 
708
- #
709
737
  # <table>
710
- #
711
738
  if re.match(r"^\s*\|.+\|\s*$", line) and not state.in_code:
712
739
  cells = [c.strip() for c in line.strip().strip("|").split("|")]
713
740
 
@@ -729,14 +756,28 @@ def parse(stream):
729
756
  yield from format_table(cells)
730
757
  continue
731
758
 
732
- #
733
759
  # <li> <ul> <ol>
734
760
  # llama-4 maverick uses + and +- for lists ... for some reason
735
- 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)
736
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
737
777
  state.in_list = True
738
778
 
739
779
  indent = len(list_item_match.group(1))
780
+
740
781
  list_type = "number" if list_item_match.group(2)[0].isdigit() else "bullet"
741
782
  content = list_item_match.group(3)
742
783
 
@@ -756,34 +797,34 @@ def parse(stream):
756
797
  if list_type == "number":
757
798
  state.ordered_list_numbers[-1] += 1
758
799
 
759
- indent = (len(state.list_item_stack) - 1) * 2
760
-
761
- wrap_width = state.Width - indent - (2 * Style.ListIndent)
762
-
763
800
  bullet = '•'
764
801
  if list_type == "number":
765
802
  list_number = int(max(state.ordered_list_numbers[-1], float(list_item_match.group(2))))
766
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)
767
810
 
768
811
  wrapped_lineList = text_wrap(content, wrap_width, Style.ListIndent,
769
- first_line_prefix = f"{(' ' * (indent ))}{FG}{Style.Symbol}{bullet}{RESET} ",
812
+ first_line_prefix = f"{(' ' * indent)}{FG}{Style.Symbol}{bullet}{RESET} ",
770
813
  subsequent_line_prefix = " " * (indent)
771
814
  )
772
815
  for wrapped_line in wrapped_lineList:
773
816
  yield f"{state.space_left()}{wrapped_line}\n"
817
+
774
818
  continue
775
- #
819
+
776
820
  # <h1> ... <h6>
777
- #
778
821
  header_match = re.match(r"^\s*(#{1,6})\s+(.*)", line)
779
822
  if header_match:
780
823
  level = len(header_match.group(1))
781
824
  yield emit_h(level, header_match.group(2))
782
825
  continue
783
826
 
784
- #
785
827
  # <hr>
786
- #
787
828
  hr_match = re.match(r"^[\s]*([-\*=_]){3,}[\s]*$", line)
788
829
  if hr_match:
789
830
  if state.last_line_empty or last_line_empty_cache:
@@ -800,7 +841,7 @@ def parse(stream):
800
841
  if len(line) == 0: yield ""
801
842
  if len(line) < state.Width:
802
843
  # we want to prevent word wrap
803
- yield f"{state.space_left()}{line_format(line)}"
844
+ yield f"{state.space_left()}{line_format(line.lstrip())}"
804
845
  else:
805
846
  wrapped_lines = text_wrap(line)
806
847
  for wrapped_line in wrapped_lines:
@@ -845,10 +886,10 @@ def emit(inp):
845
886
  else:
846
887
  chunk = buffer.pop(0)
847
888
 
848
- print(chunk, end="", flush=True)
889
+ print(chunk, end="", file=sys.stdout, flush=True)
849
890
 
850
891
  if len(buffer):
851
- print(buffer.pop(0), end="", flush=True)
892
+ print(buffer.pop(0), file=sys.stdout, end="", flush=True)
852
893
 
853
894
  def apply_multipliers(name, H, S, V):
854
895
  m = _style.get(name)
@@ -871,15 +912,16 @@ def width_calc():
871
912
  state.WidthFull = width
872
913
 
873
914
  state.Width = state.WidthFull - 2 * Style.Margin
915
+ pre = state.space_left(listwidth=True) if Style.PrettyBroken else ''
874
916
  Style.Codepad = [
875
- f"{RESET}{FG}{Style.Dark}{'▄' * state.WidthFull}{RESET}\n",
876
- 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}"
877
919
  ]
878
920
 
879
921
  def main():
880
922
  global H, S, V
881
923
 
882
- parser = ArgumentParser(description="Streamdown - A markdown renderer for modern terminals")
924
+ parser = ArgumentParser(description="Streamdown - A Streaming markdown renderer for modern terminals")
883
925
  parser.add_argument("filenameList", nargs="*", help="Input file to process (also takes stdin)")
884
926
  parser.add_argument("-l", "--loglevel", default="INFO", help="Set the logging level")
885
927
  parser.add_argument("-c", "--color", default=None, help="Set the hsv base: h,s,v")
@@ -896,20 +938,20 @@ def main():
896
938
 
897
939
  for color in ["Dark", "Mid", "Symbol", "Head", "Grey", "Bright"]:
898
940
  setattr(Style, color, apply_multipliers(color, H, S, V))
899
- for attr in ['Margin', 'ListIndent', 'Syntax']:
941
+ for attr in ['PrettyBroken', 'Margin', 'ListIndent', 'Syntax']:
900
942
  setattr(Style, attr, _style.get(attr))
901
-
943
+
902
944
  if args.scrape:
903
945
  os.makedirs(args.scrape, exist_ok=True)
904
946
  state.scrape = args.scrape
905
947
 
906
948
  Style.MarginSpaces = " " * Style.Margin
907
949
  state.WidthArg = int(args.width) or _style.get("Width") or 0
950
+ Style.Blockquote = f"{FG}{Style.Grey}│ "
908
951
  width_calc()
909
952
 
910
953
  Style.Codebg = f"{BG}{Style.Dark}"
911
954
  Style.Link = f"{FG}{Style.Symbol}{UNDERLINE[0]}"
912
- Style.Blockquote = f"{FG}{Style.Grey}│ "
913
955
 
914
956
  logging.basicConfig(stream=sys.stdout, level=args.loglevel.upper(), format=f'%(message)s')
915
957
  state.exec_master, state.exec_slave = pty.openpty()
@@ -950,15 +992,14 @@ def main():
950
992
  logging.warning(f"Exception thrown: {type(ex)} {ex}")
951
993
  traceback.print_exc()
952
994
 
953
- if state.Clipboard and state.code_buffer:
954
- code = state.code_buffer
995
+ if state.Clipboard and state.code_buffer_raw:
996
+ code = state.code_buffer_raw
955
997
  # code needs to be a base64 encoded string before emitting
956
998
  code_bytes = code.encode('utf-8')
957
999
  base64_bytes = base64.b64encode(code_bytes)
958
1000
  base64_string = base64_bytes.decode('utf-8')
959
1001
  print(f"\033]52;c;{base64_string}\a", end="", flush=True)
960
1002
 
961
-
962
1003
  if state.terminal:
963
1004
  termios.tcsetattr(sys.stdin, termios.TCSADRAIN, state.terminal)
964
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.16.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
@@ -33,7 +33,8 @@ Description-Content-Type: text/markdown
33
33
  <a href=https://pypi.org/project/streamdown><img src=https://badge.fury.io/py/streamdown.svg/></a>
34
34
  </p>
35
35
 
36
- The streaming markdown renderer for the terminal that rocks!
36
+ **The streaming markdown renderer for the terminal that rocks**
37
+
37
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.
38
39
  ```bash
39
40
  $ pip install streamdown
@@ -41,8 +42,9 @@ $ pip install streamdown
41
42
  ![Streamdown is Amazing](https://github.com/user-attachments/assets/268cb340-78cc-4df0-a773-c5ac95eceeeb)
42
43
 
43
44
  ### Provides clean copyable code for long code lines
44
- You may have noticed *inferior* renderers inject line breaks when copying code that wraps around. We're better and now you are too!
45
+ Some *inferior* renderers inject line breaks when copying code that wraps around. We're better and now you are too!
45
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!
46
48
 
47
49
  ### Supports images
48
50
  Here's kitty and alacritty. Try to do that in glow...
@@ -67,7 +69,7 @@ For instance, here is the [latex plugin](https://github.com/kristopolous/Streamd
67
69
 
68
70
  ## TOML Configuration
69
71
 
70
- 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.
71
73
 
72
74
  Here are the sections:
73
75
 
@@ -85,12 +87,15 @@ Defines the base Hue (H), Saturation (S), and Value (V) from which all other pal
85
87
  * `Margin` (integer, default: `2`): The left and right indent for the output.
86
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
87
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.
88
91
  * `ListIndent` (integer, default: `2`): This is the recursive indent for the list styles.
89
92
  * `Syntax` (string, default `monokai`): This is the syntax [highlighting theme which come via pygments](https://pygments.org/styles/).
90
93
 
91
94
  Example:
92
95
  ```toml
93
96
  [style]
97
+ PrettyPad = true
98
+ PrettyBroken = true
94
99
  HSV = [0.7, 0.5, 0.5]
95
100
  Dark = { H = 1.0, S = 1.2, V = 0.25 } # Make dark elements less saturated and darker
96
101
  Symbol = { H = 1.0, S = 1.8, V = 1.8 } # Make symbols more vibrant
@@ -103,16 +108,13 @@ Controls optional features:
103
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).
104
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.
105
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.
106
- * `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`.
107
112
 
108
113
  Example:
109
114
  ```toml
110
115
  [features]
111
116
  CodeSpaces = false
112
117
  Clipboard = false
113
- Margin = 4
114
- Width = 120
115
- Timeout = 1.0
116
118
  ```
117
119
 
118
120
  ## Command Line
@@ -146,7 +148,7 @@ Do this
146
148
  $ ./streamdown/sd.py tests/*md
147
149
 
148
150
  ## Install from source
149
- 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.
150
152
 
151
153
  $ pipx install -e .
152
154
  $ pip install -e .
@@ -154,9 +156,5 @@ After the git clone least one of these should work, hopefully. it's using the mo
154
156
 
155
157
  ### Future work
156
158
 
157
- #### CSS
158
- 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.
159
-
160
- #### scrape
161
- 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.
162
-
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=0Ug2grAsf_RWVMyDMOc0j_ZTmL8N6dOPpk24LKBPkhA,35641
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.16.0.dist-info/METADATA,sha256=sq6eIyimqRI8cZHKkNbDdP3OoYpEPYCU-v5LFhBgYfQ,7968
10
- streamdown-0.16.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
11
- streamdown-0.16.0.dist-info/entry_points.txt,sha256=HroKFsFMGf_h9PRTE96NjvjJQWupMW5TGP5RGUr1O_Q,74
12
- streamdown-0.16.0.dist-info/licenses/LICENSE.MIT,sha256=SnY46EPirUsF20dZDR8HpyVgS2_4Tjxuc6f-4OdqO7U,1070
13
- streamdown-0.16.0.dist-info/RECORD,,