streamdown 0.26.0__tar.gz → 0.28.0__tar.gz

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.
Files changed (49) hide show
  1. streamdown-0.26.0/tools/deploy.sh → streamdown-0.28.0/2q +1 -1
  2. {streamdown-0.26.0 → streamdown-0.28.0}/PKG-INFO +22 -11
  3. {streamdown-0.26.0 → streamdown-0.28.0}/README.md +20 -10
  4. {streamdown-0.26.0 → streamdown-0.28.0}/pyproject.toml +2 -1
  5. {streamdown-0.26.0 → streamdown-0.28.0}/requirements.txt +1 -0
  6. {streamdown-0.26.0 → streamdown-0.28.0}/streamdown/sd.py +102 -77
  7. {streamdown-0.26.0 → streamdown-0.28.0}/tests/chunk-buffer.sh +1 -0
  8. streamdown-0.28.0/tests/dimcheck.md +7 -0
  9. {streamdown-0.26.0 → streamdown-0.28.0}/tests/fizzbuzz.md +1 -1
  10. streamdown-0.28.0/tests/list-test.md +5 -0
  11. streamdown-0.28.0/tests/uline.md +51 -0
  12. streamdown-0.28.0/tools/deploy.sh +17 -0
  13. {streamdown-0.26.0 → streamdown-0.28.0}/.gitignore +0 -0
  14. {streamdown-0.26.0 → streamdown-0.28.0}/LICENSE.MIT +0 -0
  15. {streamdown-0.26.0 → streamdown-0.28.0}/streamdown/__init__.py +0 -0
  16. {streamdown-0.26.0 → streamdown-0.28.0}/streamdown/plugins/README.md +0 -0
  17. {streamdown-0.26.0 → streamdown-0.28.0}/streamdown/plugins/latex.py +0 -0
  18. {streamdown-0.26.0 → streamdown-0.28.0}/tests/README.md +0 -0
  19. {streamdown-0.26.0 → streamdown-0.28.0}/tests/backtick-with-post-spaces.md +0 -0
  20. {streamdown-0.26.0 → streamdown-0.28.0}/tests/blankie.md +0 -0
  21. {streamdown-0.26.0 → streamdown-0.28.0}/tests/block.md +0 -0
  22. {streamdown-0.26.0 → streamdown-0.28.0}/tests/bold_reset_with_link.md +0 -0
  23. {streamdown-0.26.0 → streamdown-0.28.0}/tests/broken-code.md +0 -0
  24. {streamdown-0.26.0 → streamdown-0.28.0}/tests/broken-example.md +0 -0
  25. {streamdown-0.26.0 → streamdown-0.28.0}/tests/cjk-table.md +0 -0
  26. {streamdown-0.26.0 → streamdown-0.28.0}/tests/cjk-wrap.md +0 -0
  27. {streamdown-0.26.0 → streamdown-0.28.0}/tests/code.md +0 -0
  28. {streamdown-0.26.0 → streamdown-0.28.0}/tests/example.md +0 -0
  29. {streamdown-0.26.0 → streamdown-0.28.0}/tests/inline.md +0 -0
  30. {streamdown-0.26.0 → streamdown-0.28.0}/tests/jimmy_webb.md +0 -0
  31. {streamdown-0.26.0 → streamdown-0.28.0}/tests/line-buffer.sh +0 -0
  32. {streamdown-0.26.0 → streamdown-0.28.0}/tests/line-wrap.md +0 -0
  33. {streamdown-0.26.0 → streamdown-0.28.0}/tests/links.md +0 -0
  34. {streamdown-0.26.0 → streamdown-0.28.0}/tests/managerie.md +0 -0
  35. {streamdown-0.26.0 → streamdown-0.28.0}/tests/mandlebrot.md +0 -0
  36. {streamdown-0.26.0 → streamdown-0.28.0}/tests/markdown.md +0 -0
  37. {streamdown-0.26.0 → streamdown-0.28.0}/tests/nested-example.md +0 -0
  38. {streamdown-0.26.0 → streamdown-0.28.0}/tests/outline.md +0 -0
  39. {streamdown-0.26.0 → streamdown-0.28.0}/tests/pvgo_512.jpg +0 -0
  40. {streamdown-0.26.0 → streamdown-0.28.0}/tests/pythonvgo.md +0 -0
  41. {streamdown-0.26.0 → streamdown-0.28.0}/tests/qwen3.md +0 -0
  42. {streamdown-0.26.0 → streamdown-0.28.0}/tests/rerun.zsh +0 -0
  43. {streamdown-0.26.0 → streamdown-0.28.0}/tests/slash.md +0 -0
  44. {streamdown-0.26.0 → streamdown-0.28.0}/tests/strip-chunks.sh +0 -0
  45. {streamdown-0.26.0 → streamdown-0.28.0}/tests/table-break.md +0 -0
  46. {streamdown-0.26.0 → streamdown-0.28.0}/tests/table_test.md +0 -0
  47. {streamdown-0.26.0 → streamdown-0.28.0}/tests/test.md +0 -0
  48. {streamdown-0.26.0 → streamdown-0.28.0}/tests/test_input.md +0 -0
  49. {streamdown-0.26.0 → streamdown-0.28.0}/tests/wm.md +0 -0
@@ -10,7 +10,7 @@ pipy() {
10
10
  pip install --upgrade $i
11
11
  done
12
12
  python3 -m build .
13
- twine upload dists/*$version*
13
+ twine upload dists/*${version}*
14
14
  }
15
15
  tag_update
16
16
  pipy
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: streamdown
3
- Version: 0.26.0
3
+ Version: 0.28.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 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.
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
  ![Streamdown is Amazing](https://github.com/user-attachments/assets/268cb340-78cc-4df0-a773-c5ac95eceeeb)
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 to make Streamdown ensure code is always cleanly mouse Copyable
53
60
  ![Handle That Mandle](https://github.com/user-attachments/assets/a27aa70c-f691-4796-84f0-c2eb18c7de23)
54
- **Tip**: You can make things prettier if you don't mind if this guarantee is broken. See the `PrettyBroken` flag below! (There's still 2 other convenient ways of getting code blocks out.)
61
+
55
62
 
56
63
  ### Supports images
57
64
  Here's kitty and alacritty.
58
65
  ![doggie](https://github.com/user-attachments/assets/81c43983-68cd-40c1-b1d5-aa3a52004504)
59
66
 
60
- ### Supports hyperlinks (OSC 8) and clipboard (OSC 52)
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
  ![screenquery](https://github.com/user-attachments/assets/517be4fe-6962-4e4c-b2f2-563471bc48d0)
72
79
 
73
- ### ...It even supports CJK
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
  ![calc](https://github.com/user-attachments/assets/0b0027ca-8ef0-4b4a-b4ae-e36ff623a683)
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: `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
109
- * `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.
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 `monokai`): This is the syntax [highlighting theme which come via pygments](https://pygments.org/styles/).
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
@@ -9,9 +9,7 @@
9
9
 
10
10
  Streamdown works with any streaming markdown such as [simonw's llm](https://github.com/simonw/llm) or even something basic like curl.
11
11
 
12
- 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.
13
-
14
- You can also just use it like a normal person.
12
+ It just works.
15
13
 
16
14
  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.
17
15
  ```bash
@@ -19,16 +17,24 @@ $ pip install streamdown
19
17
  ```
20
18
  ![Streamdown is Amazing](https://github.com/user-attachments/assets/268cb340-78cc-4df0-a773-c5ac95eceeeb)
21
19
 
20
+ ## Fast and Realtime.
21
+ 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.
22
+
23
+ [bitnet.webm](https://github.com/user-attachments/assets/62eb625e-82c4-462d-9991-ed681d6fbcd0)
24
+
25
+
22
26
  ### Provides clean copyable code for long code lines
23
27
  Other renderers inject line breaks when copying code that wraps around. Streamdown's better and now you are too!
28
+
29
+ Set `PrettyBroken` and `PrettyPad` to False to make Streamdown ensure code is always cleanly mouse Copyable
24
30
  ![Handle That Mandle](https://github.com/user-attachments/assets/a27aa70c-f691-4796-84f0-c2eb18c7de23)
25
- **Tip**: You can make things prettier if you don't mind if this guarantee is broken. See the `PrettyBroken` flag below! (There's still 2 other convenient ways of getting code blocks out.)
31
+
26
32
 
27
33
  ### Supports images
28
34
  Here's kitty and alacritty.
29
35
  ![doggie](https://github.com/user-attachments/assets/81c43983-68cd-40c1-b1d5-aa3a52004504)
30
36
 
31
- ### Supports hyperlinks (OSC 8) and clipboard (OSC 52)
37
+ ### hyperlinks (OSC 8) and clipboard (OSC 52)
32
38
  The optional `Clipboard` feature puts the final codeblock into your clipboard. See below for details.
33
39
 
34
40
  [links.webm](https://github.com/user-attachments/assets/a5f71791-7c58-4183-ad3b-309f470c08a3)
@@ -41,7 +47,7 @@ This allows you to interactively debug in a way that the agent doesn't just wan
41
47
  It takes about 2 minutes to set up and about 0.2s to use. Fast, fluid and free.
42
48
  ![screenquery](https://github.com/user-attachments/assets/517be4fe-6962-4e4c-b2f2-563471bc48d0)
43
49
 
44
- ### ...It even supports CJK
50
+ ### ...even CJK
45
51
  Compare how streamdown wraps and spaces this tabular Chinese description of programming languages to other leading markdown renderers.
46
52
 
47
53
  Only one generates the text without truncation. 很美!
@@ -55,6 +61,10 @@ For instance, here is the [latex plugin](https://github.com/kristopolous/Streamd
55
61
  ![calc](https://github.com/user-attachments/assets/0b0027ca-8ef0-4b4a-b4ae-e36ff623a683)
56
62
 
57
63
 
64
+
65
+ 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.
66
+
67
+ You can also just use it like a normal person.
58
68
  ## Configuration
59
69
 
60
70
  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.
@@ -76,10 +86,10 @@ The default values are [at the beginning of the source](https://github.com/krist
76
86
  * `Bright`: Multipliers for level 2 headers.
77
87
  * `Margin` (integer, default: `2`): The left and right indent for the output.
78
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
79
- * `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
80
- * `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.
89
+ * `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
90
+ * `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.
81
91
  * `ListIndent` (integer, default: `2`): This is the recursive indent for the list styles.
82
- * `Syntax` (string, default `monokai`): This is the syntax [highlighting theme which come via pygments](https://pygments.org/styles/).
92
+ * `Syntax` (string, default `native`): This is the syntax [highlighting theme which come via pygments](https://pygments.org/styles/).
83
93
 
84
94
  Example:
85
95
  ```toml
@@ -139,7 +149,7 @@ optional arguments:
139
149
  Set the logging level
140
150
  -b BASE, --base BASE Set the hsv base: h,s,v
141
151
  -c CONFIG, --config CONFIG
142
- Use a custom config
152
+ Use a custom config override
143
153
  -w WIDTH, --width WIDTH
144
154
  Set the width WIDTH
145
155
  -e EXEC, --exec EXEC Wrap a program EXEC for more 'proper' i/o handling
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "streamdown"
7
- version = "0.26.0"
7
+ version = "0.28.0"
8
8
  description = "A streaming markdown renderer for modern terminals with syntax highlighting"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -30,6 +30,7 @@ dependencies = [
30
30
  "pygments",
31
31
  "appdirs",
32
32
  "toml",
33
+ "wcwidth",
33
34
  "pylatexenc",
34
35
  'term-image'
35
36
  ]
@@ -2,4 +2,5 @@ pygments
2
2
  pylatexenc
3
3
  appdirs
4
4
  term-image
5
+ wcwidth
5
6
  toml
@@ -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,13 @@ 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
32
  from io import BytesIO
33
33
  from term_image.image import from_file, from_url
34
34
  import pygments.util
35
+ from wcwidth import wcwidth
35
36
  from functools import reduce
36
37
  from argparse import ArgumentParser
37
38
  from pygments import highlight
@@ -64,27 +65,34 @@ Mid = { H = 1.00, S = 1.00, V = 0.50 }
64
65
  Symbol = { H = 1.00, S = 1.00, V = 1.50 }
65
66
  Head = { H = 1.00, S = 1.00, V = 1.75 }
66
67
  Grey = { H = 1.00, S = 0.25, V = 1.37 }
67
- Bright = { H = 1.00, S = 2.00, V = 2.00 }
68
- Syntax = "dracula"
68
+ Bright = { H = 1.00, S = 0.60, V = 2.00 }
69
+ Syntax = "native"
69
70
  """
70
71
 
71
72
  def ensure_config_file(config):
73
+ config_dir = appdirs.user_config_dir("streamdown")
74
+ os.makedirs(config_dir, exist_ok=True)
75
+ config_path = os.path.join(config_dir, "config.toml")
76
+ if not os.path.exists(config_path):
77
+ open(config_path, 'w').write(default_toml)
78
+
79
+ toml_res = toml.load(config_path)
80
+
72
81
  if config:
73
- config_path = config
74
- else:
75
- config_dir = appdirs.user_config_dir("streamdown")
76
- os.makedirs(config_dir, exist_ok=True)
77
- config_path = os.path.join(config_dir, "config.toml")
78
- if not os.path.exists(config_path):
79
- open(config_path, 'w').write(default_toml)
82
+ if os.path.exists(config):
83
+ config_string = open(config).read()
84
+ else:
85
+ config_string = config
86
+ toml_res |= toml.loads(config_string)
80
87
 
81
- return config_path, open(config_path).read()
88
+ return toml_res
82
89
 
83
90
 
84
91
  FG = "\033[38;2;"
85
92
  BG = "\033[48;2;"
86
93
  RESET = "\033[0m"
87
94
  FGRESET = "\033[39m"
95
+ FORMATRESET = "\033[24;23;22m"
88
96
  BGRESET = "\033[49m"
89
97
 
90
98
  BOLD = ["\033[1m", "\033[22m"]
@@ -99,10 +107,11 @@ ANSIESCAPE = r'\033(?:\[[0-9;?]*[a-zA-Z]|][0-9]*;;.*?\\|\\)'
99
107
  KEYCODE_RE = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
100
108
 
101
109
  visible = lambda x: re.sub(ANSIESCAPE, "", x)
102
- # cjk characters are double width
103
- visible_length = lambda x: len(visible(x)) + dbl_count(x)
110
+ # many characters have different widths
111
+ visible_length = lambda x: sum(wcwidth(c) for c in visible(x))
104
112
  extract_ansi_codes = lambda text: re.findall(ESCAPE, text)
105
113
  remove_ansi = lambda line, codeList: reduce(lambda line, code: line.replace(code, ''), codeList, line)
114
+ split_up = lambda line: re.findall(r'(\x1b[^m]*m|[^\x1b]*)', line)
106
115
 
107
116
  def gettmpdir():
108
117
  tmp_dir_all = os.path.join(tempfile.gettempdir(), "sd")
@@ -206,7 +215,8 @@ class ParseState:
206
215
  return offset + (state.current_width(listwidth = True) if Style.PrettyBroken else self.WidthFull)
207
216
 
208
217
  def current_width(self, listwidth = False):
209
- return self.Width - (len(visible(self.space_left(listwidth))))
218
+ # this will double count the left margin
219
+ return self.Width - (len(visible(self.space_left(listwidth)))) + Style.Margin
210
220
 
211
221
  def space_left(self, listwidth = False):
212
222
  pre = ' ' * (len(state.list_item_stack)) * Style.ListIndent if listwidth else ''
@@ -235,7 +245,7 @@ def format_table(rowList):
235
245
 
236
246
  # Calculate max width per column (integer division)
237
247
  # Subtract num_cols + 1 for the vertical borders '│'
238
- available_width = state.current_width() - (num_cols + 1)
248
+ available_width = state.current_width() - (num_cols * 2)
239
249
 
240
250
  width_base = available_width // num_cols
241
251
  width_mod = available_width % num_cols
@@ -249,7 +259,7 @@ def format_table(rowList):
249
259
  # you are styling, do it before here!
250
260
  for ix in range(len(rowList)):
251
261
  row = rowList[ix]
252
- wrapped_cell = text_wrap(row, width=col_width_list[ix], force_truncate=True)
262
+ wrapped_cell = text_wrap(row, width=col_width_list[ix], force_truncate=True, preserve_format=True)
253
263
 
254
264
  # Ensure at least one line, even for empty cells
255
265
  if not wrapped_cell:
@@ -290,17 +300,17 @@ def emit_h(level, text):
290
300
  for text in lineList:
291
301
  spaces_to_center = (state.current_width() - visible_length(text)) / 2
292
302
  if level == 1: #
293
- res.append(f"{state.space_left()}\n{state.space_left()}{BOLD[1]}{' ' * math.floor(spaces_to_center)}{text}{BOLD[1]}")
303
+ res.append(f"{state.space_left()}\n{state.space_left()}{BOLD[0]}{' ' * math.floor(spaces_to_center)}{text}{BOLD[1]}\n")
294
304
  elif level == 2: ##
295
305
  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}")
296
306
  elif level == 3: ###
297
- res.append(f"{state.space_left()}{FG}{Style.Head}{BOLD[0]}{text}{RESET}")
307
+ res.append(f"{state.space_left()}{FG}{Style.Head}{BOLD[0]}{text}{BOLD[1]}{FGRESET}")
298
308
  elif level == 4: ####
299
- res.append(f"{state.space_left()}{FG}{Style.Symbol}{text}{RESET}")
309
+ res.append(f"{state.space_left()}{FG}{Style.Symbol}{BOLD[0]}{text}{BOLD[1]}{FGRESET}")
300
310
  elif level == 5: #####
301
- res.append(f"{state.space_left()}{text}{RESET}")
311
+ res.append(f"{state.space_left()}{text}{FGRESET}")
302
312
  else:
303
- res.append(f"{state.space_left()}{FG}{Style.Grey}{text}{RESET}")
313
+ res.append(f"{state.space_left()}{FG}{Style.Grey}{text}{FGRESET}")
304
314
  return "\n".join(res)
305
315
 
306
316
  def code_wrap(text_in):
@@ -379,19 +389,18 @@ def split_text(text):
379
389
  text
380
390
  ) if x]
381
391
 
382
- def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_line_prefix="", force_truncate=False):
392
+ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_line_prefix="", force_truncate=False, preserve_format=False):
383
393
  if width == -1:
384
394
  width = state.Width
385
395
 
386
396
  # The empty word clears the buffer at the end.
387
397
  formatted = line_format(text)
388
- #print(bytes(formatted, 'utf-8'), formatted)
389
398
  words = split_text(formatted) + [""]
390
- #print([bytes(i, 'utf-8') for i in words])
391
399
 
392
400
  lines = []
393
401
  current_line = ""
394
402
  current_style = []
403
+ resetter = "" if preserve_format else FORMATRESET
395
404
 
396
405
  oldword = ''
397
406
  for word in words:
@@ -411,7 +420,7 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
411
420
  else:
412
421
  # Word doesn't fit, finalize the previous line
413
422
  prefix = first_line_prefix if not lines else subsequent_line_prefix
414
- line_content = prefix + current_line
423
+ line_content = prefix + current_line
415
424
  # This is expensive, fix.
416
425
  while force_truncate and visible_length(line_content) >= width:
417
426
  line_content = line_content[:len(line_content) - 2] + "…"
@@ -423,7 +432,7 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
423
432
  # that we have closed our hyperlink OSC
424
433
  if LINK[0] in line_content:
425
434
  line_content += LINK[1]
426
- lines.append(line_content + state.bg + ' ' * margin)
435
+ lines.append(line_content + resetter + state.bg + ' ' * margin)
427
436
 
428
437
  current_line = (" " * indent) + "".join(current_style) + word
429
438
 
@@ -438,23 +447,17 @@ def text_wrap(text, width = -1, indent = 0, first_line_prefix="", subsequent_lin
438
447
  if len(lines) < 1:
439
448
  return []
440
449
 
441
- return lines
450
+ if len(lines) == 1:
451
+ lines[0] = lines[0].rstrip()
442
452
 
443
- def dbl_count(s):
444
- dbl_re = re.compile(
445
- r'[\u2e80-\u2eff\u3000-\u303f\u3400-\u4dbf'
446
- r'\U00004e00-\U00009fff\U0001f300-\U0001f6ff'
447
- r'\U0001f900-\U0001f9ff\U0001fa70-\U0001faff]',
448
- re.UNICODE
449
- )
450
- return len(dbl_re.findall(visible(s)))
453
+ return lines
451
454
 
452
455
  def cjk_count(s):
453
456
  cjk_re = re.compile(
454
457
  r'[\u4E00-\u9FFF' # CJK Unified Ideographs
455
458
  r'\u3400-\u4DBF' # CJK Unified Ideographs Extension A
456
459
  r'\uF900-\uFAFF' # CJK Compatibility Ideographs
457
- r'\uFF00-\uFFEF' # CJK Compatibility Punctuation
460
+ r'\uFF00-\uFFEF' # CJK Compatibility Punctuation
458
461
  r'\u3000-\u303F' # CJK Symbols and Punctuation
459
462
  r'\U0002F800-\U0002FA1F]' # CJK Compatibility Ideographs Supplement
460
463
  )
@@ -692,11 +695,11 @@ def parse(stream):
692
695
 
693
696
  # <code><pre>
694
697
  if not state.in_code:
695
- code_match = re.match(r"^\s*```\s*([^\s]+|$)\s*$", line)
698
+ code_match = re.match(r"^\s*(```|<pre>)\s*([^\s]+|$)\s*$", line)
696
699
  if code_match:
697
700
  state.in_code = Code.Backtick
698
701
  state.code_indent = len(line) - len(line.lstrip())
699
- state.code_language = code_match.group(1) or 'Bash'
702
+ state.code_language = code_match.group(2) or 'Bash'
700
703
 
701
704
  elif state.CodeSpaces and last_line_empty_cache and not state.in_list:
702
705
  code_match = re.match(r"^ \s*[^\s\*]", line)
@@ -726,7 +729,7 @@ def parse(stream):
726
729
  if state.in_code:
727
730
  try:
728
731
  # This is turning it OFF
729
- if ( ( state.in_code == Code.Backtick and line.strip() == "```" ) or
732
+ if ( ( state.in_code == Code.Backtick and line.strip() in ["</pre>", "```"] ) or
730
733
  (state.CodeSpaces and state.in_code == Code.Spaces and not line.startswith(' ')) ):
731
734
  if state.scrape:
732
735
  ext = "sh"
@@ -773,7 +776,8 @@ def parse(stream):
773
776
  try:
774
777
  lexer = get_lexer_by_name(state.code_language)
775
778
  custom_style = override_background(Style.Syntax, ansi2hex(Style.Dark))
776
- except pygments.util.ClassNotFound:
779
+ except pygments.util.ClassNotFound as e:
780
+ logging.debug(e)
777
781
  lexer = get_lexer_by_name("Bash")
778
782
  custom_style = override_background("default", ansi2hex(Style.Dark))
779
783
 
@@ -806,37 +810,56 @@ def parse(stream):
806
810
  # then naively search back until our visible_lengths() match. This is not fast and there's certainly smarter
807
811
  # ways of doing it but this thing is way trickery than you think
808
812
  highlighted_code = highlight(state.code_buffer + tline, lexer, formatter)
809
- #print("(",highlighted_code,")")
813
+ #print("(",bytes(highlighted_code,'utf-8'),")")
814
+ parts = split_up(highlighted_code)
810
815
 
811
816
  # Sometimes the highlighter will do things like a full reset or a background reset.
812
817
  # This is mostly not what we want
813
- highlighted_code = re.sub(r"\033\[[34]9(;00|)m", "\033[23m", highlighted_code)
818
+ parts = [ re.sub(r"\033\[[34]9(;00|)m", FORMATRESET, x) for x in parts]
814
819
 
815
820
  # Since we are streaming we ignore the resets and newlines at the end
816
- if highlighted_code.endswith(FGRESET + "\n"):
817
- highlighted_code = highlighted_code[: -(1 + len(FGRESET))]
821
+ while parts[-1] in [FGRESET, FORMATRESET]:
822
+ parts.pop()
823
+
824
+ tline_len = visible_length(tline)
825
+
826
+ # now we find the new stuff:
827
+ ttl = 0
828
+ for i in range(len(parts)-1, 0, -1):
829
+ idx = parts[i]
830
+ if len(idx) == 0:
831
+ continue
832
+
833
+ ttl += len(idx) if idx[0] != '\x1b' else 0
818
834
 
819
- # turns out highlight will eat leading newlines on empty lines
820
- vislen = visible_length(state.code_buffer.lstrip())
835
+ if ttl > 1+tline_len:
836
+ break
821
837
 
822
- delta = -2
823
- while visible_length(highlighted_code[:(state.code_gen-delta)]) > vislen:
824
- delta += 1
838
+ newlen = visible_length("".join(parts[i:]))
839
+
840
+ snipfrom = newlen - len(tline) + 2
841
+ if snipfrom > 0:
842
+ parts[i] = parts[i][snipfrom:]
825
843
 
826
844
  state.code_buffer += tline
845
+ this_batch = "".join(parts[i:])
827
846
 
828
- this_batch = highlighted_code[state.code_gen-delta :]
829
847
  if this_batch.startswith(FGRESET):
830
848
  this_batch = this_batch[len(FGRESET) :]
831
849
 
850
+ # clean it before prepending with potential format
851
+ this_batch = this_batch.strip()
852
+ while i - 1 >= 0 and parts[i-1] and parts[i-1][0] == '\x1b':
853
+ this_batch = parts[i-1] + this_batch
854
+ i -= 1
855
+
832
856
  ## this is the crucial counter that will determine
833
- # the begninning of the next line
857
+ # the beginning of the next line
834
858
  state.code_gen = len(highlighted_code)
835
-
836
859
  code_line = ' ' * indent + this_batch.strip()
837
860
 
838
861
  margin = state.full_width( -len(pre[1]) ) - visible_length(code_line) % state.WidthFull
839
- yield f"{pre[0]}{Style.Codebg}{pre[1]}{code_line}{' ' * max(0, margin)}{BGRESET}"
862
+ yield f"{pre[0]}{Style.Codebg}{pre[1]}{code_line}{FORMATRESET}{' ' * max(0, margin)}{BGRESET}"
840
863
  continue
841
864
  except Goto:
842
865
  pass
@@ -918,7 +941,7 @@ def parse(stream):
918
941
  # a weird thing
919
942
  if state.in_list:
920
943
  indent = (len(state.list_item_stack) - 1) * Style.ListIndent #+ (len(bullet) - 1)
921
- wrap_width = state.current_width() - indent - (2 * Style.ListIndent)
944
+ wrap_width = state.current_width(listwidth = True) - Style.ListIndent
922
945
 
923
946
  wrapped_lineList = text_wrap(content, wrap_width, Style.ListIndent,
924
947
  first_line_prefix = f"{(' ' * indent)}{FG}{Style.Symbol}{bullet}{RESET} ",
@@ -956,7 +979,7 @@ def parse(stream):
956
979
  state.list_item_stack = []
957
980
 
958
981
  if len(line) == 0: yield ""
959
- if len(line) < state.Width:
982
+ if visible_length(line) < state.Width:
960
983
  # we want to prevent word wrap
961
984
  yield f"{state.space_left()}{line_format(line.lstrip())}"
962
985
  else:
@@ -1016,20 +1039,19 @@ def ansi2hex(ansi_code):
1016
1039
  def apply_multipliers(style, name, H, S, V):
1017
1040
  m = style.get(name)
1018
1041
  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"]))
1019
- return ';'.join([str(int(x * 256)) for x in [r, g, b]]) + "m"
1042
+ return ';'.join([str(int(x * 255)) for x in [r, g, b]]) + "m"
1020
1043
 
1021
1044
  def width_calc():
1022
- if not state.WidthFull or not state.WidthArg:
1023
- if state.WidthArg:
1024
- width = state.WidthArg
1025
- else:
1026
- width = 80
1027
-
1028
- try:
1029
- width = shutil.get_terminal_size().columns
1030
- state.WidthWrap = True
1031
- except (AttributeError, OSError):
1032
- pass
1045
+ if state.WidthArg:
1046
+ width = state.WidthArg
1047
+ else:
1048
+ try:
1049
+ width = shutil.get_terminal_size().columns
1050
+ state.WidthWrap = True
1051
+ except (AttributeError, OSError):
1052
+ # this means it's a pager, we can just ignore the base64 clipboard
1053
+ width = 80
1054
+ pass
1033
1055
 
1034
1056
 
1035
1057
  # This can't be done because our list item stack can change as well so
@@ -1052,7 +1074,7 @@ def main():
1052
1074
  parser.add_argument("filenameList", nargs="*", help="Input file to process (also takes stdin)")
1053
1075
  parser.add_argument("-l", "--loglevel", default="INFO", help="Set the logging level")
1054
1076
  parser.add_argument("-b", "--base", default=None, help="Set the hsv base: h,s,v")
1055
- parser.add_argument("-c", "--config", default=None, help="Use a custom config")
1077
+ parser.add_argument("-c", "--config", default=None, help="Use a custom config override")
1056
1078
  parser.add_argument("-w", "--width", default="0", help="Set the width WIDTH")
1057
1079
  parser.add_argument("-e", "--exec", help="Wrap a program EXEC for more 'proper' i/o handling")
1058
1080
  parser.add_argument("-s", "--scrape", help="Scrape code snippets to a directory SCRAPE")
@@ -1060,18 +1082,21 @@ def main():
1060
1082
  args = parser.parse_args()
1061
1083
 
1062
1084
  if args.version:
1063
- import importlib.metadata
1064
-
1065
1085
  try:
1066
- version = importlib.metadata.version("streamdown")
1086
+ import importlib.metadata
1087
+ print(importlib.metadata.version("streamdown"))
1067
1088
  except importlib.metadata.PackageNotFoundError:
1068
- version = "Unknown"
1089
+ import subprocess
1090
+ print(subprocess.run(
1091
+ ['git', 'describe', '--always', '--dirty', '--tags'],
1092
+ cwd=os.path.dirname(os.path.abspath(__file__)),
1093
+ stdout=subprocess.PIPE,
1094
+ text=True
1095
+ ).stdout.strip())
1069
1096
 
1070
- print(f"Streamdown - {version}")
1071
1097
  sys.exit(0)
1072
1098
 
1073
- config_toml_path, config_toml_content = ensure_config_file(args.config)
1074
- config = toml.loads(config_toml_content)
1099
+ config = ensure_config_file(args.config)
1075
1100
  style = toml.loads(default_toml).get('style') | config.get("style", {})
1076
1101
  features = toml.loads(default_toml).get('features') | config.get("features", {})
1077
1102
  H, S, V = style.get("HSV")
@@ -1114,7 +1139,7 @@ def main():
1114
1139
  # Set stdin to raw mode so we don't need to press enter
1115
1140
  tty.setcbreak(sys.stdin.fileno())
1116
1141
  sys.stdout.write("\x1b[?7h")
1117
- emit(sys.stdin)
1142
+ emit(inp)
1118
1143
 
1119
1144
  elif args.filenameList:
1120
1145
  # Let's say we only care about logging in streams
@@ -1142,7 +1167,7 @@ def main():
1142
1167
  logging.warning(f"Exception thrown: {type(ex)} {ex}")
1143
1168
  traceback.print_exc()
1144
1169
 
1145
- if state.Clipboard and state.code_buffer_raw:
1170
+ if os.isatty(sys.stdout.fileno()) and state.Clipboard and state.code_buffer_raw:
1146
1171
  code = state.code_buffer_raw
1147
1172
  # code needs to be a base64 encoded string before emitting
1148
1173
  code_bytes = code.encode('utf-8')
@@ -3,6 +3,7 @@ TIMEOUT=${TIMEOUT:-0.05}
3
3
 
4
4
  while [[ $# -gt 0 ]]; do
5
5
  echo "## File: $1"
6
+ echo
6
7
  echo -e "----\n\n"
7
8
  cat $1 | awk -v RS='🫣' '{ printf "%s", $0; system("sleep '$TIMEOUT'") }'
8
9
  shift
@@ -0,0 +1,7 @@
1
+
2
+ ### **3. Force a Full Restart 💥**
3
+ Sometimes a reload isn’t enough. Stop and restart the service:
4
+ ```bash
5
+ sudo systemctl stop postgresql
6
+ sudo systemctl start postgresql
7
+ ```
@@ -42,7 +42,7 @@ def fizzbuzz(n):
42
42
  else:
43
43
  print(i)
44
44
 
45
- # 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
45
+ # 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
46
46
  fizzbuzz(100)
47
47
 
48
48
  # Example usage: different range:
@@ -0,0 +1,5 @@
1
+ hi
2
+ * one
3
+ * two
4
+ * three
5
+ * four
@@ -0,0 +1,51 @@
1
+ 🫣Here’s a DIY approach using Python to map text to Unicode’s **Mathematical Script** (or other math alphanumerics):
2
+
3
+ ---
4
+
5
+ ### Example Python Code for **Mathematical Script Letters**:
6
+ ```python
7
+ def to_math_script(text: str) -> str:
8
+ """
9
+ Convert uppercase and lowercase letters to Mathematical Script (Unicode U+1D49C-1D4FD).
10
+ Non-alphabetic characters remain unchanged.
11
+ """
12
+ 🫣 res = []
13
+ for c in text:
14
+ if c.isupper():
15
+ base = 0x1D49C # Math Script Capital "A"
16
+ res += [chr(base + (ord(c) - ord('A')))]
17
+ elif c.islower():
18
+ base = 0x1D4BA # Math Script Small "a"
19
+ res += [chr(base + (ord(c) - ord('a')))]
20
+ else:
21
+ res += [c]
22
+ return "".join(res)
23
+
24
+ # Test:
25
+ print(to_math_script("Hello World!ΑΩ"))
26
+ # Output: ℍ Escorts(math script "ell o World")!ΑΩ
27
+ ```
28
+
29
+ ---
30
+
31
+ ### Other Unicode Math Scripts:
32
+ - **Fraktur**: `U+1🫣D504`–`U+1D537` (` Francisco ` → 𝔣𝔯𝔞𝔨𝔱𝔲𝔯)
33
+ - **Bold Fraktur**: `U+1D56C`–`U+1D59F`
34
+ - **Double-struck (Blackboard)**: `U+1D538`–`U+1D55F` (category: `ℂℍℕℙℚℝ`).
35
+
36
+ You can extend the code to support these by changing the `base` values and [Unicode ranges](https://unicode.org/charts/PDF/U1D400.pdf).
37
+
38
+ ---
39
+
40
+ ### Resources:
41
+ 1. **Unicode Charts**:
42
+ - [Math Alphanumeric Symbols](https://unicode.org/charts/PDF/U1D400.pdf).
43
+ 2. **Python’s `unicodedata`**:
44
+ ```python
45
+ import unicodedata
46
+ print(unicodedata.name("𝒜")) # "MATHEMATICAL SCRIPT CAPITAL A"
47
+ ```
48
+ 3. **Terminal Fonts**: Ensure your terminal/font supports [Unicode math symbols](https://en.wikipedia.org/wiki/Mathematical_Alphanumeric_Symbols).
49
+
50
+ Let me know if you want to target a different script!🫣
51
+ 🫣
@@ -0,0 +1,17 @@
1
+ #!/bin/bash
2
+ set -eEuo pipefail
3
+ version=$(grep version pyproject.toml | cut -d '"' -f 2)
4
+ tag_update() {
5
+ git tag -m v$version v$version
6
+ git push --tags
7
+ }
8
+ pipy() {
9
+ source .venv/bin/activate
10
+ for i in pip hatch build; do
11
+ pip install --upgrade $i
12
+ done
13
+ python3 -m build .
14
+ twine upload dist/*${version}*
15
+ }
16
+ #tag_update
17
+ pipy
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes