openmd 1.3.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.
openmd-1.3.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 RufusLin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
openmd-1.3.0/PKG-INFO ADDED
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: openmd
3
+ Version: 1.3.0
4
+ Summary: Fast Markdown previewer for macOS with GitHub-dark theme, sidebar TOC, and multi-file tabs
5
+ Author: RufusLin
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/RufusLin/openmd
8
+ Project-URL: Repository, https://github.com/RufusLin/openmd
9
+ Project-URL: Issues, https://github.com/RufusLin/openmd/issues
10
+ Keywords: markdown,preview,viewer,macos,pyside6
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: MacOS X
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Text Processing :: Markup
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: PySide6
23
+ Requires-Dist: Markdown
24
+ Requires-Dist: beautifulsoup4
25
+ Dynamic: license-file
26
+
27
+ # openmd
28
+
29
+ A fast, minimal Markdown previewer for macOS with a GitHub-dark theme, collapsible sidebar TOC, live reload, Mermaid diagrams, KaTeX math, and multi-file tab support.
30
+ Built this for myself because who needs to fire up VS Code or Cursor just to quickly view a pretty printed Markdown file, right?🤣
31
+
32
+ ![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue) ![License: MIT](https://img.shields.io/badge/license-MIT-green) ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey) ![PyPI version](https://img.shields.io/pypi/v/openmd)
33
+
34
+ **GitHub:** [RufusLin/openmd](https://github.com/RufusLin/openmd)
35
+ **Warning - Lazy Maintainer:** Really bad at reading PRs, but will pay attention to issues to fix bugs. Feel free to fork, remember to give credit, please.
36
+
37
+ ---
38
+
39
+ ## Usage
40
+
41
+ ```bash
42
+ # Open a single file
43
+ openmd README.md
44
+
45
+ # Open multiple files (each in its own tab)
46
+ openmd doc1.md doc2.md doc3.md
47
+
48
+ # No arguments — interactive picker (choose from .md files in current directory)
49
+ openmd
50
+
51
+ # Glob expansion
52
+ openmd docs/*.md
53
+ ```
54
+
55
+ ### Shell aliases (optional)
56
+
57
+ Add to your `~/.zshrc` or `~/.bashrc` for quick access:
58
+
59
+ ```zsh
60
+ # Local preview — opens in background
61
+ localmd() {
62
+ openmd "$@" >/dev/null 2>&1 &
63
+ }
64
+
65
+ # Remote preview via SSH (requires a 'home' SSH alias in ~/.ssh/config)
66
+ remotemd() {
67
+ local remote_path="$1"
68
+ local filename=$(basename "$remote_path")
69
+ local tmp_file="/tmp/remote_preview_${filename}.md"
70
+ scp "home:$remote_path" "$tmp_file" && \
71
+ openmd "$tmp_file" >/dev/null 2>&1 &
72
+ }
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Features
78
+
79
+ - **GitHub-dark theme** — comfortable reading in low-light environments. BUT! Change it to whatever you like in `.openmd.css`
80
+ - **Live reload** — the preview updates instantly when the file is saved; no manual refresh needed
81
+ - **Mermaid diagrams** — fenced ` ```mermaid ` blocks render automatically via CDN
82
+ - **KaTeX math** — Formulae? inline `$…$` and display `$$…$$` expressions render out of the box
83
+ - **Collapsible sidebar TOC** — hierarchical, of course; click any heading to jump to it
84
+ - **Multi-file tabs** — pass multiple `.md` files (even `*.md` globs) and each opens in its own tab, max 6
85
+ - **Interactive file picker** — run with no arguments and choose from `.md` files in the current directory via a curses-based picker — no need to copy and paste file names
86
+ - **Remote preview** — optional `remotemd` shell alias pulls a file from a remote host via `scp` and opens it instantly
87
+ - **Keyboard shortcuts** — `Esc` closes the window; arrow keys navigate the sidebar
88
+
89
+ ---
90
+
91
+ ## Requirements
92
+
93
+ - macOS (uses PySide6/Qt WebEngine)
94
+ - Python 3.8+
95
+ - [PySide6](https://pypi.org/project/PySide6/)
96
+ - [Markdown](https://pypi.org/project/Markdown/)
97
+ - [BeautifulSoup4](https://pypi.org/project/beautifulsoup4/)
98
+
99
+ ---
100
+
101
+ ## Installation
102
+
103
+ ### pip (recommended)
104
+
105
+ ```bash
106
+ pip install openmd
107
+ ```
108
+
109
+ After installing, the `openmd` command is available in your shell.
110
+
111
+ ### From source
112
+
113
+ ```bash
114
+ git clone https://github.com/RufusLin/openmd.git
115
+ cd openmd
116
+ pip install -e .
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Live reload
122
+
123
+ Openmd watches the opened file for changes using Qt's `QFileSystemWatcher`. Save the file in any editor (vim, neovim, VS Code, etc.) and the preview — including the sidebar TOC — updates instantly with no manual refresh.
124
+
125
+ ## Mermaid & KaTeX
126
+
127
+ Mermaid and KaTeX are loaded automatically from CDN on every render. No configuration required.
128
+
129
+ **Mermaid example:**
130
+ ````markdown
131
+ ```mermaid
132
+ graph TD
133
+ A[Start] --> B{Decision}
134
+ B -->|Yes| C[Do it]
135
+ B -->|No| D[Skip]
136
+ ```
137
+ ````
138
+
139
+ **KaTeX example:**
140
+ ```markdown
141
+ Inline: $E = mc^2$
142
+
143
+ Display:
144
+ $$\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}$$
145
+ ```
146
+
147
+ > **Note:** Mermaid and KaTeX require an internet connection to load from CDN. Offline rendering is not currently supported (maybe never, come to think of it😉).
148
+
149
+ ---
150
+
151
+ ## Keyboard shortcuts
152
+
153
+ | Key | Action |
154
+ |-----|--------|
155
+ | `Esc` | Close the preview window |
156
+ | `↑` / `↓` | Navigate the sidebar TOC |
157
+ | Click heading | Jump to that section |
158
+
159
+ ---
160
+
161
+ ## License
162
+
163
+ MIT
openmd-1.3.0/README.md ADDED
@@ -0,0 +1,137 @@
1
+ # openmd
2
+
3
+ A fast, minimal Markdown previewer for macOS with a GitHub-dark theme, collapsible sidebar TOC, live reload, Mermaid diagrams, KaTeX math, and multi-file tab support.
4
+ Built this for myself because who needs to fire up VS Code or Cursor just to quickly view a pretty printed Markdown file, right?🤣
5
+
6
+ ![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue) ![License: MIT](https://img.shields.io/badge/license-MIT-green) ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey) ![PyPI version](https://img.shields.io/pypi/v/openmd)
7
+
8
+ **GitHub:** [RufusLin/openmd](https://github.com/RufusLin/openmd)
9
+ **Warning - Lazy Maintainer:** Really bad at reading PRs, but will pay attention to issues to fix bugs. Feel free to fork, remember to give credit, please.
10
+
11
+ ---
12
+
13
+ ## Usage
14
+
15
+ ```bash
16
+ # Open a single file
17
+ openmd README.md
18
+
19
+ # Open multiple files (each in its own tab)
20
+ openmd doc1.md doc2.md doc3.md
21
+
22
+ # No arguments — interactive picker (choose from .md files in current directory)
23
+ openmd
24
+
25
+ # Glob expansion
26
+ openmd docs/*.md
27
+ ```
28
+
29
+ ### Shell aliases (optional)
30
+
31
+ Add to your `~/.zshrc` or `~/.bashrc` for quick access:
32
+
33
+ ```zsh
34
+ # Local preview — opens in background
35
+ localmd() {
36
+ openmd "$@" >/dev/null 2>&1 &
37
+ }
38
+
39
+ # Remote preview via SSH (requires a 'home' SSH alias in ~/.ssh/config)
40
+ remotemd() {
41
+ local remote_path="$1"
42
+ local filename=$(basename "$remote_path")
43
+ local tmp_file="/tmp/remote_preview_${filename}.md"
44
+ scp "home:$remote_path" "$tmp_file" && \
45
+ openmd "$tmp_file" >/dev/null 2>&1 &
46
+ }
47
+ ```
48
+
49
+ ---
50
+
51
+ ## Features
52
+
53
+ - **GitHub-dark theme** — comfortable reading in low-light environments. BUT! Change it to whatever you like in `.openmd.css`
54
+ - **Live reload** — the preview updates instantly when the file is saved; no manual refresh needed
55
+ - **Mermaid diagrams** — fenced ` ```mermaid ` blocks render automatically via CDN
56
+ - **KaTeX math** — Formulae? inline `$…$` and display `$$…$$` expressions render out of the box
57
+ - **Collapsible sidebar TOC** — hierarchical, of course; click any heading to jump to it
58
+ - **Multi-file tabs** — pass multiple `.md` files (even `*.md` globs) and each opens in its own tab, max 6
59
+ - **Interactive file picker** — run with no arguments and choose from `.md` files in the current directory via a curses-based picker — no need to copy and paste file names
60
+ - **Remote preview** — optional `remotemd` shell alias pulls a file from a remote host via `scp` and opens it instantly
61
+ - **Keyboard shortcuts** — `Esc` closes the window; arrow keys navigate the sidebar
62
+
63
+ ---
64
+
65
+ ## Requirements
66
+
67
+ - macOS (uses PySide6/Qt WebEngine)
68
+ - Python 3.8+
69
+ - [PySide6](https://pypi.org/project/PySide6/)
70
+ - [Markdown](https://pypi.org/project/Markdown/)
71
+ - [BeautifulSoup4](https://pypi.org/project/beautifulsoup4/)
72
+
73
+ ---
74
+
75
+ ## Installation
76
+
77
+ ### pip (recommended)
78
+
79
+ ```bash
80
+ pip install openmd
81
+ ```
82
+
83
+ After installing, the `openmd` command is available in your shell.
84
+
85
+ ### From source
86
+
87
+ ```bash
88
+ git clone https://github.com/RufusLin/openmd.git
89
+ cd openmd
90
+ pip install -e .
91
+ ```
92
+
93
+ ---
94
+
95
+ ## Live reload
96
+
97
+ Openmd watches the opened file for changes using Qt's `QFileSystemWatcher`. Save the file in any editor (vim, neovim, VS Code, etc.) and the preview — including the sidebar TOC — updates instantly with no manual refresh.
98
+
99
+ ## Mermaid & KaTeX
100
+
101
+ Mermaid and KaTeX are loaded automatically from CDN on every render. No configuration required.
102
+
103
+ **Mermaid example:**
104
+ ````markdown
105
+ ```mermaid
106
+ graph TD
107
+ A[Start] --> B{Decision}
108
+ B -->|Yes| C[Do it]
109
+ B -->|No| D[Skip]
110
+ ```
111
+ ````
112
+
113
+ **KaTeX example:**
114
+ ```markdown
115
+ Inline: $E = mc^2$
116
+
117
+ Display:
118
+ $$\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}$$
119
+ ```
120
+
121
+ > **Note:** Mermaid and KaTeX require an internet connection to load from CDN. Offline rendering is not currently supported (maybe never, come to think of it😉).
122
+
123
+ ---
124
+
125
+ ## Keyboard shortcuts
126
+
127
+ | Key | Action |
128
+ |-----|--------|
129
+ | `Esc` | Close the preview window |
130
+ | `↑` / `↓` | Navigate the sidebar TOC |
131
+ | Click heading | Jump to that section |
132
+
133
+ ---
134
+
135
+ ## License
136
+
137
+ MIT
@@ -0,0 +1,163 @@
1
+ Metadata-Version: 2.4
2
+ Name: openmd
3
+ Version: 1.3.0
4
+ Summary: Fast Markdown previewer for macOS with GitHub-dark theme, sidebar TOC, and multi-file tabs
5
+ Author: RufusLin
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/RufusLin/openmd
8
+ Project-URL: Repository, https://github.com/RufusLin/openmd
9
+ Project-URL: Issues, https://github.com/RufusLin/openmd/issues
10
+ Keywords: markdown,preview,viewer,macos,pyside6
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: MacOS X
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: MacOS
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Topic :: Text Processing :: Markup
18
+ Classifier: Topic :: Utilities
19
+ Requires-Python: >=3.8
20
+ Description-Content-Type: text/markdown
21
+ License-File: LICENSE
22
+ Requires-Dist: PySide6
23
+ Requires-Dist: Markdown
24
+ Requires-Dist: beautifulsoup4
25
+ Dynamic: license-file
26
+
27
+ # openmd
28
+
29
+ A fast, minimal Markdown previewer for macOS with a GitHub-dark theme, collapsible sidebar TOC, live reload, Mermaid diagrams, KaTeX math, and multi-file tab support.
30
+ Built this for myself because who needs to fire up VS Code or Cursor just to quickly view a pretty printed Markdown file, right?🤣
31
+
32
+ ![Python 3.8+](https://img.shields.io/badge/python-3.8%2B-blue) ![License: MIT](https://img.shields.io/badge/license-MIT-green) ![Platform: macOS](https://img.shields.io/badge/platform-macOS-lightgrey) ![PyPI version](https://img.shields.io/pypi/v/openmd)
33
+
34
+ **GitHub:** [RufusLin/openmd](https://github.com/RufusLin/openmd)
35
+ **Warning - Lazy Maintainer:** Really bad at reading PRs, but will pay attention to issues to fix bugs. Feel free to fork, remember to give credit, please.
36
+
37
+ ---
38
+
39
+ ## Usage
40
+
41
+ ```bash
42
+ # Open a single file
43
+ openmd README.md
44
+
45
+ # Open multiple files (each in its own tab)
46
+ openmd doc1.md doc2.md doc3.md
47
+
48
+ # No arguments — interactive picker (choose from .md files in current directory)
49
+ openmd
50
+
51
+ # Glob expansion
52
+ openmd docs/*.md
53
+ ```
54
+
55
+ ### Shell aliases (optional)
56
+
57
+ Add to your `~/.zshrc` or `~/.bashrc` for quick access:
58
+
59
+ ```zsh
60
+ # Local preview — opens in background
61
+ localmd() {
62
+ openmd "$@" >/dev/null 2>&1 &
63
+ }
64
+
65
+ # Remote preview via SSH (requires a 'home' SSH alias in ~/.ssh/config)
66
+ remotemd() {
67
+ local remote_path="$1"
68
+ local filename=$(basename "$remote_path")
69
+ local tmp_file="/tmp/remote_preview_${filename}.md"
70
+ scp "home:$remote_path" "$tmp_file" && \
71
+ openmd "$tmp_file" >/dev/null 2>&1 &
72
+ }
73
+ ```
74
+
75
+ ---
76
+
77
+ ## Features
78
+
79
+ - **GitHub-dark theme** — comfortable reading in low-light environments. BUT! Change it to whatever you like in `.openmd.css`
80
+ - **Live reload** — the preview updates instantly when the file is saved; no manual refresh needed
81
+ - **Mermaid diagrams** — fenced ` ```mermaid ` blocks render automatically via CDN
82
+ - **KaTeX math** — Formulae? inline `$…$` and display `$$…$$` expressions render out of the box
83
+ - **Collapsible sidebar TOC** — hierarchical, of course; click any heading to jump to it
84
+ - **Multi-file tabs** — pass multiple `.md` files (even `*.md` globs) and each opens in its own tab, max 6
85
+ - **Interactive file picker** — run with no arguments and choose from `.md` files in the current directory via a curses-based picker — no need to copy and paste file names
86
+ - **Remote preview** — optional `remotemd` shell alias pulls a file from a remote host via `scp` and opens it instantly
87
+ - **Keyboard shortcuts** — `Esc` closes the window; arrow keys navigate the sidebar
88
+
89
+ ---
90
+
91
+ ## Requirements
92
+
93
+ - macOS (uses PySide6/Qt WebEngine)
94
+ - Python 3.8+
95
+ - [PySide6](https://pypi.org/project/PySide6/)
96
+ - [Markdown](https://pypi.org/project/Markdown/)
97
+ - [BeautifulSoup4](https://pypi.org/project/beautifulsoup4/)
98
+
99
+ ---
100
+
101
+ ## Installation
102
+
103
+ ### pip (recommended)
104
+
105
+ ```bash
106
+ pip install openmd
107
+ ```
108
+
109
+ After installing, the `openmd` command is available in your shell.
110
+
111
+ ### From source
112
+
113
+ ```bash
114
+ git clone https://github.com/RufusLin/openmd.git
115
+ cd openmd
116
+ pip install -e .
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Live reload
122
+
123
+ Openmd watches the opened file for changes using Qt's `QFileSystemWatcher`. Save the file in any editor (vim, neovim, VS Code, etc.) and the preview — including the sidebar TOC — updates instantly with no manual refresh.
124
+
125
+ ## Mermaid & KaTeX
126
+
127
+ Mermaid and KaTeX are loaded automatically from CDN on every render. No configuration required.
128
+
129
+ **Mermaid example:**
130
+ ````markdown
131
+ ```mermaid
132
+ graph TD
133
+ A[Start] --> B{Decision}
134
+ B -->|Yes| C[Do it]
135
+ B -->|No| D[Skip]
136
+ ```
137
+ ````
138
+
139
+ **KaTeX example:**
140
+ ```markdown
141
+ Inline: $E = mc^2$
142
+
143
+ Display:
144
+ $$\int_0^\infty e^{-x^2} dx = \frac{\sqrt{\pi}}{2}$$
145
+ ```
146
+
147
+ > **Note:** Mermaid and KaTeX require an internet connection to load from CDN. Offline rendering is not currently supported (maybe never, come to think of it😉).
148
+
149
+ ---
150
+
151
+ ## Keyboard shortcuts
152
+
153
+ | Key | Action |
154
+ |-----|--------|
155
+ | `Esc` | Close the preview window |
156
+ | `↑` / `↓` | Navigate the sidebar TOC |
157
+ | Click heading | Jump to that section |
158
+
159
+ ---
160
+
161
+ ## License
162
+
163
+ MIT
@@ -0,0 +1,10 @@
1
+ LICENSE
2
+ README.md
3
+ openmd.py
4
+ pyproject.toml
5
+ openmd.egg-info/PKG-INFO
6
+ openmd.egg-info/SOURCES.txt
7
+ openmd.egg-info/dependency_links.txt
8
+ openmd.egg-info/entry_points.txt
9
+ openmd.egg-info/requires.txt
10
+ openmd.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ openmd = openmd:main
@@ -0,0 +1,3 @@
1
+ PySide6
2
+ Markdown
3
+ beautifulsoup4
@@ -0,0 +1 @@
1
+ openmd
openmd-1.3.0/openmd.py ADDED
@@ -0,0 +1,387 @@
1
+ #!/usr/bin/env python3
2
+ # Version: 1.3.0
3
+ # Added hierarchical QTreeWidget TOC sidebar (H1→top, H2→children, H3→grandchildren).
4
+ # Tabs are intentionally preserved — DO NOT remove the QTabWidget multi-file tab view.
5
+ # openmd.py - Simple Markdown previewer with sidebar TOC
6
+ # -------------------------------------------------
7
+ # This script is invoked by shell aliases defined in ~/.zshrc:
8
+ #
9
+ # localmd() {
10
+ # $MD_VIEWER_PY $MD_VIEWER_SCRIPT "$@" >/dev/null 2>&1 &
11
+ # }
12
+ #
13
+ # remotemd() {
14
+ # local remote_path="$1"
15
+ # local filename=$(basename "$remote_path")
16
+ # local tmp_file="/tmp/remote_preview_${filename}.md"
17
+ #
18
+ # # Pull via the 'home' alias, then launch
19
+ # scp "home:$remote_path" "$tmp_file" && \
20
+ # $MD_VIEWER_PY $MD_VIEWER_SCRIPT "$tmp_file" >/dev/null 2>&1 &
21
+ # }
22
+ #
23
+ # The script itself only opens a local file path; remote handling is performed by the
24
+ # `remotemd` alias which copies the file via SSH (scp) to a temporary location and
25
+ # then runs this script on that copy. No glob expansion or remote file fetching is
26
+ # performed inside the Python code.
27
+ # -------------------------------------------------
28
+
29
+ import sys, os, markdown
30
+
31
+ # Try to import curses for file picker; fallback to simple list
32
+ try:
33
+ import curses
34
+ except Exception:
35
+ curses = None
36
+
37
+ from PySide6.QtWidgets import (
38
+ QApplication, QMainWindow, QTabWidget, QWidget, QVBoxLayout,
39
+ QSplitter, QTreeWidget, QTreeWidgetItem,
40
+ )
41
+ from PySide6.QtWebEngineWidgets import QWebEngineView
42
+ from PySide6.QtCore import QSize, Qt, QFileSystemWatcher
43
+ from bs4 import BeautifulSoup
44
+
45
+ # GitHub-Modern Dark Theme
46
+ CSS = """body {
47
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
48
+ line-height: 1.6; color: #c9d1d9; max-width: 900px; margin: auto; padding: 2rem; background-color: #0d1117;
49
+ }
50
+ pre {
51
+ background-color: #161b22; padding: 16px; border-radius: 6px; border: 1px solid #30363d;
52
+ overflow: auto; font-family: "SFMono-Regular", Consolas, monospace;
53
+ }
54
+ code { background-color: rgba(110,118,129,0.4); padding: 0.2em 0.4em; border-radius: 6px; font-size: 85%; }
55
+ table { border-collapse: collapse; width: 100%; margin: 24px 0; border: 1px solid #30363d; }
56
+ table th, table td { border: 1px solid #30363d; padding: 8px 12px; }
57
+ table tr:nth-child(even) { background-color: #161b22; }
58
+ """
59
+
60
+ # Sidebar CSS injected into the preview HTML
61
+ SIDEBAR_CSS = """
62
+ QTreeWidget {
63
+ background: #161b22;
64
+ color: #c9d1d9;
65
+ border-right: 1px solid #30363d;
66
+ font-size: 13px;
67
+ }
68
+ QTreeWidget::item:hover { background: #212730; }
69
+ QTreeWidget::item:selected { background: #1f6feb; color: #ffffff; }
70
+ """
71
+
72
+
73
+ # ---------------------------------------------------------------------------
74
+ # Per-file preview window: sidebar TOC (QTreeWidget) + QWebEngineView
75
+ # ---------------------------------------------------------------------------
76
+ # NOTE: This class renders a single markdown file with a collapsible sidebar.
77
+ # The outer QTabWidget (in __main__) wraps multiple FilePreviewWidget instances
78
+ # so that multi-file tab support is preserved. DO NOT collapse these into one.
79
+ # ---------------------------------------------------------------------------
80
+
81
+ class FilePreviewWidget(QWidget):
82
+ """A single-file preview pane: left sidebar TOC + right HTML view."""
83
+
84
+ def __init__(self, file_path: str):
85
+ super().__init__()
86
+ self.file_path = file_path
87
+
88
+ # KaTeX CDN snippets — loaded synchronously in the HTML head/body
89
+ # KaTeX renders fine via inline scripts in Qt WebEngine setHtml()
90
+ self._katex_css = '<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.css">'
91
+ self._katex_script = (
92
+ '<script src="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.js"></script>\n'
93
+ '<script src="https://cdn.jsdelivr.net/npm/katex@0.16/dist/contrib/auto-render.min.js"></script>\n'
94
+ '<script>'
95
+ 'document.addEventListener("DOMContentLoaded",function(){'
96
+ 'if(window.renderMathInElement)renderMathInElement(document.body,{'
97
+ 'delimiters:[{left:"$$",right:"$$",display:true},{left:"$",right:"$",display:false}]'
98
+ '});});'
99
+ '</script>'
100
+ )
101
+ # Mermaid: Qt WebEngine setHtml() does not reliably fire window.onload or
102
+ # DOMContentLoaded for CDN scripts. Instead we load the script in the HTML
103
+ # and trigger mermaid.run() via page().runJavaScript() on the loadFinished signal.
104
+ self._mermaid_script = '<script src="https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.min.js"></script>'
105
+
106
+ # --- Load and render markdown ---
107
+ html_body, toc_html = self._render_markdown(file_path)
108
+ full_html = self._build_html(html_body)
109
+
110
+ # --- Sidebar (QTreeWidget) ---
111
+ # Collapsible TOC: H1 → top-level, H2 → children, H3 → grandchildren.
112
+ # Arrow keys navigate; Enter jumps to the heading; Esc closes the window.
113
+ # DO NOT remove this sidebar — it is the primary navigation feature.
114
+ self.sidebar = QTreeWidget()
115
+ self.sidebar.setHeaderHidden(True)
116
+ self.sidebar.setIndentation(16)
117
+ self.sidebar.setStyleSheet(SIDEBAR_CSS)
118
+ self.sidebar.setFixedWidth(220)
119
+ self.sidebar.itemClicked.connect(self._jump_to_section)
120
+ self._populate_sidebar(toc_html)
121
+
122
+ # --- Web view ---
123
+ self.view = QWebEngineView()
124
+ # Connect loadFinished to trigger Mermaid rendering after the page and CDN
125
+ # scripts have fully loaded. This is the only reliable way to run Mermaid
126
+ # in Qt WebEngine when content is set via setHtml().
127
+ self.view.loadFinished.connect(self._on_load_finished)
128
+ self.view.setHtml(full_html)
129
+
130
+ # --- Live reload: single watcher per file, connected to _reload ---
131
+ # DO NOT create a second QFileSystemWatcher — one per FilePreviewWidget only.
132
+ self.watcher = QFileSystemWatcher(self)
133
+ self.watcher.addPath(self.file_path)
134
+ self.watcher.fileChanged.connect(self._reload)
135
+
136
+ # --- Layout: sidebar left, preview right ---
137
+ splitter = QSplitter(Qt.Horizontal)
138
+ splitter.addWidget(self.sidebar)
139
+ splitter.addWidget(self.view)
140
+ splitter.setStretchFactor(0, 0) # sidebar: fixed
141
+ splitter.setStretchFactor(1, 1) # preview: stretches
142
+
143
+ layout = QVBoxLayout(self)
144
+ layout.setContentsMargins(0, 0, 0, 0)
145
+ layout.addWidget(splitter)
146
+
147
+ def _populate_sidebar(self, toc_html: str):
148
+ """Parse the TOC div from markdown-with-toc output and fill the QTreeWidget."""
149
+ if not toc_html:
150
+ return
151
+ soup = BeautifulSoup(toc_html, "html.parser")
152
+ toc_div = soup.find("div", class_="toc")
153
+ if not toc_div:
154
+ return
155
+ # Top-level <ul> inside the TOC div
156
+ top_ul = toc_div.find("ul", recursive=False)
157
+ if top_ul:
158
+ for li in top_ul.find_all("li", recursive=False):
159
+ self._add_toc_item(li, parent_item=None)
160
+ self.sidebar.expandAll()
161
+
162
+ def _add_toc_item(self, node, parent_item):
163
+ """Recursively add a <li> node (and its nested <ul> children) to the sidebar."""
164
+ anchor_tag = node.find("a")
165
+ if not anchor_tag:
166
+ return
167
+ title = anchor_tag.get_text()
168
+ anchor_id = anchor_tag.get("name") or anchor_tag.get("href", "").lstrip("#")
169
+ if not anchor_id:
170
+ return
171
+
172
+ item = QTreeWidgetItem([title])
173
+ item.setData(0, Qt.UserRole, anchor_id)
174
+
175
+ if parent_item is None:
176
+ self.sidebar.addTopLevelItem(item)
177
+ else:
178
+ parent_item.addChild(item)
179
+
180
+ # Recurse into nested <ul> for sub-headings
181
+ for child_ul in node.find_all("ul", recursive=False):
182
+ for sub_li in child_ul.find_all("li", recursive=False):
183
+ self._add_toc_item(sub_li, item)
184
+
185
+ def _render_markdown(self, file_path: str):
186
+ """Read and render a markdown file; return (html_body, toc_html).
187
+
188
+ Uses the Markdown class directly so we can access md.toc, which is the
189
+ only reliable way to get the TOC HTML — the markdown.markdown() one-liner
190
+ does NOT generate the <div class='toc'> block.
191
+ """
192
+ try:
193
+ with open(file_path, 'r', encoding='utf-8') as fh:
194
+ raw = fh.read()
195
+ md = markdown.Markdown(
196
+ extensions=['toc', 'extra', 'sane_lists'],
197
+ extension_configs={'toc': {'permalink': False, 'anchorlink': True}},
198
+ )
199
+ html_body = md.convert(raw)
200
+ toc_html = md.toc # e.g. '<div class="toc"><ul>...</ul></div>'
201
+ # Convert markdown-rendered mermaid code blocks to the format Mermaid v10 expects.
202
+ # The markdown library renders ```mermaid as <pre><code class="language-mermaid">...
203
+ # but Mermaid.run() looks for <pre class="mermaid">...</pre>.
204
+ # Use BeautifulSoup to do this safely without affecting other code blocks.
205
+ soup = BeautifulSoup(html_body, 'html.parser')
206
+ for code_tag in soup.find_all('code', class_='language-mermaid'):
207
+ pre_tag = code_tag.parent
208
+ if pre_tag and pre_tag.name == 'pre':
209
+ # Replace <pre><code class="language-mermaid">...</code></pre>
210
+ # with <pre class="mermaid">...</pre>
211
+ new_pre = soup.new_tag('pre', **{'class': 'mermaid'})
212
+ new_pre.string = code_tag.get_text()
213
+ pre_tag.replace_with(new_pre)
214
+ html_body = str(soup)
215
+ except Exception as e:
216
+ html_body = f"<h1>Error loading {os.path.basename(file_path)}</h1><p>{type(e).__name__}: {e}</p>"
217
+ toc_html = ""
218
+ return html_body, toc_html
219
+
220
+ def _build_html(self, html_body: str) -> str:
221
+ """Wrap rendered markdown body with CSS, Mermaid, and KaTeX."""
222
+ return (
223
+ f"<!DOCTYPE html><html><head><meta charset='utf-8'><style>{CSS}</style>{self._katex_css}</head>"
224
+ f"<body>{html_body}{self._mermaid_script}{self._katex_script}</body></html>"
225
+ )
226
+
227
+ def _on_load_finished(self, ok: bool):
228
+ """Called by loadFinished signal after setHtml() completes.
229
+
230
+ Triggers Mermaid rendering via runJavaScript — the only reliable way
231
+ to run CDN-loaded Mermaid in Qt WebEngine after setHtml().
232
+ """
233
+ if ok:
234
+ self.view.page().runJavaScript(
235
+ "if(window.mermaid){"
236
+ " mermaid.initialize({startOnLoad:false,theme:'dark'});"
237
+ " mermaid.run();"
238
+ "}"
239
+ )
240
+
241
+ def _reload(self, _path: str = ""):
242
+ """Called by QFileSystemWatcher when the watched file changes."""
243
+ # Re-add path: some editors (vim, neovim) replace the file atomically,
244
+ # which removes it from the watcher. Re-adding ensures continued watching.
245
+ self.watcher.addPath(self.file_path)
246
+ html_body, toc_html = self._render_markdown(self.file_path)
247
+ self.sidebar.clear()
248
+ self._populate_sidebar(toc_html)
249
+ self.view.setHtml(self._build_html(html_body))
250
+
251
+ def _jump_to_section(self, item: QTreeWidgetItem):
252
+ """Scroll the web view to the heading whose anchor matches the clicked item."""
253
+ anchor = item.data(0, Qt.UserRole)
254
+ if anchor:
255
+ self.view.page().runJavaScript(
256
+ f"var el = document.getElementById('{anchor}'); if (el) el.scrollIntoView();"
257
+ )
258
+
259
+
260
+ # ---------------------------------------------------------------------------
261
+ # Top-level window: wraps one or more FilePreviewWidget tabs
262
+ # ---------------------------------------------------------------------------
263
+ # IMPORTANT: The QTabWidget multi-file tab view is intentional and must be
264
+ # preserved. When multiple .md files are passed on the command line, each
265
+ # opens in its own tab (with its own sidebar). DO NOT collapse to single-file.
266
+ # ---------------------------------------------------------------------------
267
+
268
+ class MDPreviewWindow(QMainWindow):
269
+ def __init__(self, tab_widget: QTabWidget):
270
+ super().__init__()
271
+ self.setCentralWidget(tab_widget)
272
+ self.setWindowTitle("MD Preview")
273
+ self.resize(QSize(1200, 1000))
274
+ self.tab_widget = tab_widget
275
+
276
+ def keyPressEvent(self, event):
277
+ if event.key() == Qt.Key_Escape:
278
+ QApplication.quit()
279
+ else:
280
+ super().keyPressEvent(event)
281
+
282
+
283
+ # ---------------------------------------------------------------------------
284
+ # Helpers
285
+ # ---------------------------------------------------------------------------
286
+
287
+ def is_markdown(path: str) -> bool:
288
+ return path.lower().endswith('.md')
289
+
290
+
291
+ def pick_file_curses() -> str:
292
+ """Return a selected .md file from the current directory using curses."""
293
+ md_files = [f for f in os.listdir('.') if is_markdown(f) and os.path.isfile(f)]
294
+ md_files.sort()
295
+ if not md_files:
296
+ sys.exit("No markdown files found in the current directory.")
297
+ selected = 0
298
+ if curses:
299
+ def draw(stdscr):
300
+ nonlocal selected
301
+ stdscr.keypad(True)
302
+ curses.curs_set(0) # hide cursor
303
+ while True:
304
+ stdscr.clear()
305
+ stdscr.addstr(0, 0, "Select a Markdown file (\u2191\u2193 navigate, Enter select, Esc quit)")
306
+ for idx, f in enumerate(md_files):
307
+ y = 2 + idx
308
+ if y >= curses.LINES - 1:
309
+ break
310
+ stdscr.addstr(y, 2, f"{'>' if idx == selected else ' '} {f}")
311
+ stdscr.refresh()
312
+ key = stdscr.getch()
313
+ if key == curses.KEY_UP and selected > 0:
314
+ selected -= 1
315
+ elif key == curses.KEY_DOWN and selected < len(md_files) - 1:
316
+ selected += 1
317
+ elif key in (curses.KEY_ENTER, 10, 13):
318
+ return
319
+ elif key == 27: # Esc
320
+ sys.exit(0)
321
+ try:
322
+ curses.wrapper(draw)
323
+ except SystemExit:
324
+ raise
325
+ except Exception:
326
+ pass
327
+ else:
328
+ for i, f in enumerate(md_files, 1):
329
+ print(f"{i}. {f}")
330
+ choice = input("Select number (or ENTER to cancel): ").strip()
331
+ if not choice:
332
+ sys.exit(0)
333
+ try:
334
+ idx = int(choice) - 1
335
+ if 0 <= idx < len(md_files):
336
+ selected = idx
337
+ else:
338
+ sys.exit(0)
339
+ except ValueError:
340
+ sys.exit(0)
341
+ return md_files[selected]
342
+
343
+
344
+ # ---------------------------------------------------------------------------
345
+ # Entry point
346
+ # ---------------------------------------------------------------------------
347
+
348
+ def main():
349
+ """Entry point — used both by direct invocation and by the pip console script."""
350
+ # No arguments → interactive curses picker
351
+ if len(sys.argv) < 2:
352
+ file_path = pick_file_curses()
353
+ md_files = [file_path]
354
+ else:
355
+ # Collect all arguments (shell may have expanded globs)
356
+ files = sys.argv[1:]
357
+ # Filter to markdown files only
358
+ md_files = [f for f in files if is_markdown(f)]
359
+ if not md_files:
360
+ sys.exit("No markdown files matched the given pattern(s).")
361
+ # Limit to 6 files (one tab per file)
362
+ if len(md_files) > 6:
363
+ sys.stderr.write("Warning: more than 6 files supplied; showing first 6.\n")
364
+ md_files = md_files[:6]
365
+
366
+ # -----------------------------------------------------------------
367
+ # Build the tabbed window.
368
+ # Each file gets its own tab containing a FilePreviewWidget (sidebar
369
+ # TOC + web view). The QTabWidget is intentional — DO NOT remove it.
370
+ # Wrapped in a top-level try/except to surface Qt init failures.
371
+ # -----------------------------------------------------------------
372
+ try:
373
+ app = QApplication(sys.argv)
374
+ tab_widget = QTabWidget()
375
+ for f in md_files:
376
+ widget = FilePreviewWidget(f)
377
+ tab_widget.addTab(widget, os.path.basename(f))
378
+ window = MDPreviewWindow(tab_widget)
379
+ window.show()
380
+ sys.exit(app.exec())
381
+ except Exception as e:
382
+ sys.stderr.write(f"Fatal error while building the preview window: {type(e).__name__}: {e}\n")
383
+ sys.exit(1)
384
+
385
+
386
+ if __name__ == "__main__":
387
+ main()
@@ -0,0 +1,39 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "openmd"
7
+ version = "1.3.0"
8
+ description = "Fast Markdown previewer for macOS with GitHub-dark theme, sidebar TOC, and multi-file tabs"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "RufusLin" }]
12
+ requires-python = ">=3.8"
13
+ dependencies = [
14
+ "PySide6",
15
+ "Markdown",
16
+ "beautifulsoup4",
17
+ ]
18
+ keywords = ["markdown", "preview", "viewer", "macos", "pyside6"]
19
+ classifiers = [
20
+ "Development Status :: 4 - Beta",
21
+ "Environment :: MacOS X",
22
+ "Intended Audience :: Developers",
23
+ "License :: OSI Approved :: MIT License",
24
+ "Operating System :: MacOS",
25
+ "Programming Language :: Python :: 3",
26
+ "Topic :: Text Processing :: Markup",
27
+ "Topic :: Utilities",
28
+ ]
29
+
30
+ [project.scripts]
31
+ openmd = "openmd:main"
32
+
33
+ [project.urls]
34
+ Homepage = "https://github.com/RufusLin/openmd"
35
+ Repository = "https://github.com/RufusLin/openmd"
36
+ Issues = "https://github.com/RufusLin/openmd/issues"
37
+
38
+ [tool.setuptools]
39
+ py-modules = ["openmd"]
openmd-1.3.0/setup.cfg ADDED
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+