termflow-md 0.2.2__tar.gz → 0.2.4__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 (62) hide show
  1. {termflow_md-0.2.2 → termflow_md-0.2.4}/CHANGELOG.md +19 -0
  2. {termflow_md-0.2.2 → termflow_md-0.2.4}/PKG-INFO +1 -1
  3. {termflow_md-0.2.2 → termflow_md-0.2.4}/pyproject.toml +1 -1
  4. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/__init__.py +1 -1
  5. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/render/style.py +35 -0
  6. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/tui/__init__.py +7 -1
  7. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/tui/terminal.py +45 -0
  8. termflow_md-0.2.4/tests/test_style.py +28 -0
  9. {termflow_md-0.2.2 → termflow_md-0.2.4}/tests/test_tui.py +46 -0
  10. {termflow_md-0.2.2 → termflow_md-0.2.4}/uv.lock +1 -1
  11. {termflow_md-0.2.2 → termflow_md-0.2.4}/.github/workflows/ci.yml +0 -0
  12. {termflow_md-0.2.2 → termflow_md-0.2.4}/.github/workflows/publish.yml +0 -0
  13. {termflow_md-0.2.2 → termflow_md-0.2.4}/.gitignore +0 -0
  14. {termflow_md-0.2.2 → termflow_md-0.2.4}/LICENSE +0 -0
  15. {termflow_md-0.2.2 → termflow_md-0.2.4}/README.md +0 -0
  16. {termflow_md-0.2.2 → termflow_md-0.2.4}/demo.py +0 -0
  17. {termflow_md-0.2.2 → termflow_md-0.2.4}/examples/config.toml +0 -0
  18. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/__main__.py +0 -0
  19. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/ansi/__init__.py +0 -0
  20. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/ansi/codes.py +0 -0
  21. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/ansi/color.py +0 -0
  22. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/ansi/style.py +0 -0
  23. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/ansi/utils.py +0 -0
  24. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/cli.py +0 -0
  25. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/config/__init__.py +0 -0
  26. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/config/config.py +0 -0
  27. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/core/__init__.py +0 -0
  28. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/core/enums.py +0 -0
  29. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/core/state.py +0 -0
  30. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/parser/__init__.py +0 -0
  31. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/parser/entities.py +0 -0
  32. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/parser/events.py +0 -0
  33. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/parser/inline.py +0 -0
  34. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/parser/parser.py +0 -0
  35. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/render/__init__.py +0 -0
  36. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/render/code.py +0 -0
  37. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/render/heading.py +0 -0
  38. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/render/list.py +0 -0
  39. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/render/renderer.py +0 -0
  40. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/render/table.py +0 -0
  41. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/render/text.py +0 -0
  42. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/stream/__init__.py +0 -0
  43. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/stream/smooth.py +0 -0
  44. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/syntax/__init__.py +0 -0
  45. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/syntax/highlighter.py +0 -0
  46. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/themes/__init__.py +0 -0
  47. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/themes/osc.py +0 -0
  48. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/themes/palette.py +0 -0
  49. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/tui/keys.py +0 -0
  50. {termflow_md-0.2.2 → termflow_md-0.2.4}/termflow/tui/menu.py +0 -0
  51. {termflow_md-0.2.2 → termflow_md-0.2.4}/test_document.md +0 -0
  52. {termflow_md-0.2.2 → termflow_md-0.2.4}/tests/__init__.py +0 -0
  53. {termflow_md-0.2.2 → termflow_md-0.2.4}/tests/conftest.py +0 -0
  54. {termflow_md-0.2.2 → termflow_md-0.2.4}/tests/test_ansi.py +0 -0
  55. {termflow_md-0.2.2 → termflow_md-0.2.4}/tests/test_config.py +0 -0
  56. {termflow_md-0.2.2 → termflow_md-0.2.4}/tests/test_inline.py +0 -0
  57. {termflow_md-0.2.2 → termflow_md-0.2.4}/tests/test_integration.py +0 -0
  58. {termflow_md-0.2.2 → termflow_md-0.2.4}/tests/test_parser.py +0 -0
  59. {termflow_md-0.2.2 → termflow_md-0.2.4}/tests/test_renderer.py +0 -0
  60. {termflow_md-0.2.2 → termflow_md-0.2.4}/tests/test_smooth.py +0 -0
  61. {termflow_md-0.2.2 → termflow_md-0.2.4}/tests/test_syntax.py +0 -0
  62. {termflow_md-0.2.2 → termflow_md-0.2.4}/tests/test_themes.py +0 -0
@@ -7,6 +7,25 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.2.4] - 2026-08-22
11
+
12
+ ### Added
13
+
14
+ - `RenderStyle.from_palette()` — derive rendering accents from a
15
+ terminal palette's ANSI slots (Palette object or plain dict), so
16
+ menus and markdown output stay on-theme when the app has remapped
17
+ terminal colors via OSC instead of using the hardcoded defaults.
18
+
19
+ ## [0.2.3] - 2026-08-22
20
+
21
+ ### Added
22
+
23
+ - `termflow.tui.terminal_session()` — refcounted, thread-safe context
24
+ manager holding raw mode + ONE alternate screen across multiple TUI
25
+ components. Chained menu flows (a picker opening sub-pickers) run each
26
+ menu with `alt_screen=False` inside a session and never flash the
27
+ primary screen between menus. Nested sessions share the outermost one.
28
+
10
29
  ## [0.2.2] - 2026-08-22
11
30
 
12
31
  ### Fixed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: termflow-md
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: A streaming markdown renderer for modern terminals
5
5
  Project-URL: Homepage, https://github.com/mpfaffenberger/termflow
6
6
  Project-URL: Repository, https://github.com/mpfaffenberger/termflow
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "termflow-md"
7
- version = "0.2.2"
7
+ version = "0.2.4"
8
8
  description = "A streaming markdown renderer for modern terminals"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -31,7 +31,7 @@ CLI usage:
31
31
  $ tf --style dracula README.md
32
32
  """
33
33
 
34
- __version__ = "0.2.2"
34
+ __version__ = "0.2.4"
35
35
 
36
36
  from termflow.config import Config
37
37
  from termflow.parser import Parser
@@ -43,6 +43,41 @@ class RenderStyle:
43
43
  """Create default style."""
44
44
  return cls()
45
45
 
46
+ @classmethod
47
+ def from_palette(cls, palette) -> RenderStyle:
48
+ """Derive a style from a terminal palette's ANSI slots.
49
+
50
+ Accepts a :class:`termflow.themes.Palette` or any mapping/object
51
+ exposing ``ansi`` (16 #rrggbb strings, slots 0-15) and optionally
52
+ ``bg``. Keeps rendered output on-theme when the application has
53
+ remapped the terminal colors (OSC 4/10/11): accents come from the
54
+ palette's bright slots instead of the hardcoded defaults.
55
+
56
+ Slots with missing colors fall back to the default style's value.
57
+ """
58
+ if isinstance(palette, dict):
59
+ ansi = list(palette.get("ansi") or [])
60
+ bg = palette.get("bg")
61
+ else:
62
+ ansi = list(getattr(palette, "ansi", ()) or ())
63
+ bg = getattr(palette, "bg", None)
64
+ base = cls()
65
+
66
+ def slot(i: int, fallback: str) -> str:
67
+ return ansi[i] if i < len(ansi) and ansi[i] else fallback
68
+
69
+ return cls(
70
+ bright=slot(12, base.bright), # bright blue - primary accent
71
+ head=slot(10, base.head), # bright green - secondary
72
+ symbol=slot(5, base.symbol), # magenta - markers, borders
73
+ grey=slot(8, base.grey), # bright black - dim text
74
+ dark=bg or slot(0, base.dark), # background-adjacent
75
+ mid=slot(0, base.mid), # block backgrounds
76
+ light=slot(14, base.light), # bright cyan - light accent
77
+ link=slot(12, base.link), # match primary accent
78
+ error=slot(9, base.error), # bright red
79
+ )
80
+
46
81
  @classmethod
47
82
  def from_hue(cls, hue: float) -> RenderStyle:
48
83
  """Generate a style from a base hue (0.0-1.0).
@@ -10,7 +10,12 @@ prompt_toolkit -- just escape sequences and small, injectable pieces.
10
10
 
11
11
  from termflow.tui.keys import Key, parse_key, read_key
12
12
  from termflow.tui.menu import Menu, MenuBuilder, MenuItem, MenuResult
13
- from termflow.tui.terminal import alt_screen, raw_mode, terminal_size
13
+ from termflow.tui.terminal import (
14
+ alt_screen,
15
+ raw_mode,
16
+ terminal_session,
17
+ terminal_size,
18
+ )
14
19
 
15
20
  __all__ = [
16
21
  "Key",
@@ -22,5 +27,6 @@ __all__ = [
22
27
  "parse_key",
23
28
  "raw_mode",
24
29
  "read_key",
30
+ "terminal_session",
25
31
  "terminal_size",
26
32
  ]
@@ -12,6 +12,7 @@ from __future__ import annotations
12
12
  import contextlib
13
13
  import shutil
14
14
  import sys
15
+ import threading
15
16
  from typing import IO, TYPE_CHECKING
16
17
 
17
18
  if TYPE_CHECKING:
@@ -75,6 +76,50 @@ def raw_mode(stream: IO[str] | None = None) -> Iterator[None]:
75
76
  termios.tcsetattr(fd, termios.TCSADRAIN, old)
76
77
 
77
78
 
79
+ _session_lock = threading.Lock()
80
+ _session_depth = 0
81
+ _session_stack: contextlib.ExitStack | None = None
82
+
83
+
84
+ @contextlib.contextmanager
85
+ def terminal_session(output: IO[str] | None = None) -> Iterator[None]:
86
+ """Hold raw mode + ONE alternate screen across multiple TUI components.
87
+
88
+ Menus normally enter/exit the alternate screen per ``run()``; chained
89
+ flows (a picker opening sub-pickers) would flash the primary screen
90
+ between every menu. Wrap the whole flow in a ``terminal_session`` and
91
+ run each menu with ``alt_screen=False``: the session owns the screen
92
+ and raw mode, the menus just paint.
93
+
94
+ Reentrant and refcounted (thread-safe): nested sessions share the
95
+ outermost one, so components can wrap themselves defensively without
96
+ caring whether a parent already holds the terminal.
97
+ """
98
+ global _session_depth, _session_stack
99
+ with _session_lock:
100
+ _session_depth += 1
101
+ if _session_depth == 1:
102
+ stack = contextlib.ExitStack()
103
+ try:
104
+ stack.enter_context(raw_mode())
105
+ stack.enter_context(alt_screen(output))
106
+ except Exception:
107
+ _session_depth -= 1
108
+ with contextlib.suppress(Exception):
109
+ stack.close()
110
+ raise
111
+ _session_stack = stack
112
+ try:
113
+ yield
114
+ finally:
115
+ with _session_lock:
116
+ _session_depth -= 1
117
+ if _session_depth == 0 and _session_stack is not None:
118
+ stack, _session_stack = _session_stack, None
119
+ with contextlib.suppress(Exception):
120
+ stack.close()
121
+
122
+
78
123
  @contextlib.contextmanager
79
124
  def alt_screen(output: IO[str] | None = None, hide_cursor: bool = True) -> Iterator[None]:
80
125
  """Switch to the alternate screen buffer (like vim/less) for the duration.
@@ -0,0 +1,28 @@
1
+ class TestFromPalette:
2
+ def test_derives_accents_from_ansi_slots(self):
3
+ from termflow.render.style import RenderStyle
4
+ from termflow.themes import get_palette
5
+
6
+ pal = get_palette("purple_puppy")
7
+ style = RenderStyle.from_palette(pal)
8
+ assert style.bright == pal.ansi[12]
9
+ assert style.symbol == pal.ansi[5]
10
+ assert style.error == pal.ansi[9]
11
+ assert style.dark == pal.bg
12
+
13
+ def test_accepts_plain_dict(self):
14
+ from termflow.render.style import RenderStyle
15
+
16
+ ansi = [f"#0000{i:02x}" for i in range(16)]
17
+ style = RenderStyle.from_palette({"ansi": ansi, "bg": "#101010"})
18
+ assert style.bright == ansi[12]
19
+ assert style.grey == ansi[8]
20
+ assert style.dark == "#101010"
21
+
22
+ def test_missing_slots_fall_back_to_defaults(self):
23
+ from termflow.render.style import RenderStyle
24
+
25
+ base = RenderStyle.default()
26
+ style = RenderStyle.from_palette({"ansi": []})
27
+ assert style.bright == base.bright
28
+ assert style.error == base.error
@@ -9,6 +9,52 @@ from termflow.tui import Key, MenuBuilder, MenuItem, parse_key
9
9
  # =============================================================================
10
10
  # Key parsing
11
11
  # =============================================================================
12
+ class TestTerminalSession:
13
+ """Refcounted raw-mode + alt-screen session for chained menus."""
14
+
15
+ def test_single_session_enters_and_exits_once(self):
16
+ from io import StringIO
17
+
18
+ from termflow.tui import terminal_session
19
+
20
+ out = StringIO()
21
+ with terminal_session(out):
22
+ pass
23
+ assert out.getvalue().count("\x1b[?1049h") == 1
24
+ assert out.getvalue().count("\x1b[?1049l") == 1
25
+
26
+ def test_nested_sessions_share_one_alt_screen(self):
27
+ from io import StringIO
28
+
29
+ from termflow.tui import terminal_session
30
+
31
+ out = StringIO()
32
+ with terminal_session(out):
33
+ with terminal_session(out):
34
+ pass
35
+ # Inner exit must NOT drop back to the primary screen.
36
+ assert out.getvalue().count("\x1b[?1049l") == 0
37
+ assert out.getvalue().count("\x1b[?1049h") == 1
38
+ assert out.getvalue().count("\x1b[?1049l") == 1
39
+
40
+ def test_session_releases_on_exception(self):
41
+ from io import StringIO
42
+
43
+ from termflow.tui import terminal_session
44
+
45
+ out = StringIO()
46
+ try:
47
+ with terminal_session(out):
48
+ raise RuntimeError("boom")
49
+ except RuntimeError:
50
+ pass
51
+ assert out.getvalue().count("\x1b[?1049l") == 1
52
+ # Depth back to zero: a fresh session works.
53
+ with terminal_session(out):
54
+ pass
55
+ assert out.getvalue().count("\x1b[?1049h") == 2
56
+
57
+
12
58
  class TestParseKey:
13
59
  def test_printable_passthrough(self):
14
60
  assert parse_key("a") == "a"
@@ -747,7 +747,7 @@ wheels = [
747
747
 
748
748
  [[package]]
749
749
  name = "termflow-md"
750
- version = "0.2.1"
750
+ version = "0.2.3"
751
751
  source = { editable = "." }
752
752
  dependencies = [
753
753
  { name = "pygments" },
File without changes
File without changes
File without changes
File without changes
File without changes