Forked from PyMarkdownEditor by Clinton Wright.
Upstream focuses on an editor-first architecture with future plugin support.
This fork integrates focus sessions directly to provide timesheet-grade logging for daily consulting workflows.
A PyQt6 Markdown editor with built-in focus sessions and timesheet logging for consultants and deep-work workflows.
Markdown editing is commodity.
This fork prioritizes integrated focus sessions that auto-generate structured Markdown timesheets — answering:
- What you worked on
- When you worked
- How long it actually took
- Session notes and interruptions
All captured locally and appended directly to your notes.
This editor supports Focus Sessions: timed work blocks that auto-append structured Markdown timesheets into your notes.
Owner-led governance; contributions welcome (see CONTRIBUTING).
-
Live preview
Debounced, side-by-side Markdown preview while you type. -
Markdown rendering
Usespython-markdownwith common extensions:extra,fenced_code,codehilite,toc,sane_lists,smarty, and supports extra extensions such aspymdown-extensions(e.g. math/LaTeX viaarithmatex). -
Dark-mode aware CSS
Preview uses theme-friendly styles that work in both light and dark environments. -
Robust file handling
- Open/Save
.mdwith atomic writes (QSaveFile, UTF-8). - Recent files persisted via
QSettings. - Drag & drop files onto the window to open them.
- Open/Save
-
Exporters
- HTML – saves the preview HTML as-is.
- PDF (classic) – QTextDocument/QPrinter (A4, 12.7 mm margins) to mirror the preview.
- PDF (WebEngine) – optional QWebEngine-based exporter (
web_pdf_exporter.py) for closer "what you see is what you print" output when Qt WebEngine is available.
-
Editor helpers
- One-click bold, italic,
`inline code`. - Heading helpers:
# H1,## H2. - List helper:
- list. - Insert link dialog.
- Insert image dialog.
- Insert table dialog.
- Find / Replace / Replace all dialog.
- Toggle wrap and preview from toolbar/menu.
- One-click bold, italic,
-
Pomodoro focus timer + session notes
Timermenu with: Start Focus Session..., Pause / Resume, Stop & Save, Timer Settings....- Start flow collects title, tag, preset (
25/5,50/10, or custom), and target folder. - Start on a new note or continue on the current note.
- Detached floating timer window (always-on-top) with countdown + Pause/Stop controls.
- Timer color states: green (normal), amber (warning), red (final stretch).
- Auto-save active session note every N minutes (default
2), and on pause/stop/app close. - Session summary updates in-place (pure Markdown) with a consultant-friendly format:
- what you worked on, 2) when, 3) actual vs expected duration, 4) interruption count, 5) interruption time cost.
- Includes hidden machine-friendly metadata (
focus-meta) for future exports/charts/reporting. - Alarm options: built-in profiles (
Beep,Chime,Bell,Ping) or custom sound file with preview. - Timer settings persist locally on disk (no reconfiguration each launch).
- Appends local JSONL session logs to
~/.focusforge/logs/YYYY-MM-DD.jsonl.
Quick demo: starting a focus session, live timer, and auto-generated Markdown summary.
-
Architecture
- SOLID-leaning design with clear boundaries.
- Simple dependency injection container.
- Strategy-based exporters registered in an
ExporterRegistrysingleton. - Thin Qt UI (
MainWindow+ dialogs) that delegates to services.
# 1) Create and activate a virtual environment python -m venv .venv # Windows . .venv/Scripts/activate # macOS / Linux source .venv/bin/activate # 2) Install runtime dependencies pip install -r requirements.txt # 3) Run the app python -m pymd
Python: 3.10+ recommended.
Once the package is live on PyPI under py-markdown-editor, installation will look like:
pip install py-markdown-editor python -m pymd
If you later add a console script entry point, this could become as simple as:
pymd
Runtime dependencies (see requirements.txt for exact versions):
PyQt6>=6.6 Markdown>=3.5 Pygments>=2.17 pymdown-extensions
For WebEngine-based PDF export, you may also need:
PyQt6-WebEngineand the corresponding Qt WebEngine system libraries on your platform.
Dev/test tools live in dev-requirements.txt—see Testing.
Core actions
- New / Open / Save / Save As – standard platform shortcuts.
- Toggle wrap – toolbar/menu.
- Toggle preview – toolbar/menu.
- Quit – standard platform shortcut.
Text & formatting
-
Toolbar/menu helpers:
**B**– bold.*i*– italic.`code`– inline code.# H1,## H2– heading prefixes.- list– bullet list prefix.
Insert / dialogs
- Insert link... – opens the link dialog.
- Insert image... – opens a file chooser and inserts an image reference.
- Insert table... – opens the table dialog to generate Markdown tables.
- Start Focus Session... – opens the pomodoro/session setup dialog.
- Timer Settings... – configure auto-save interval, sound enablement, alarm profile/custom sound with preview, and default session folder.
- About... – opens the About dialog.
Find & replace
- Find... – open the find dialog.
- Replace... – open find/replace dialog.
- Replace all – replace all matches in the document.
Timer
- Pause / Resume – pauses or resumes the active focus session.
- Stop & Save – stops the active session and writes a session log entry.
- Floating Timer Window – detached always-on-top countdown with Pause/Stop buttons.
- Active Session Lock – while a session is active, opening/switching to other notes is blocked to keep session capture consistent.
(Exact keyboard accelerators may vary slightly by platform/Qt style, but all are exposed via menus and toolbars.)
-
Consultant-ready time capture
Turn regular Markdown notes into lightweight session evidence you can use for reporting and billing support. -
Lower context-switch overhead
Timer and notes live in one tool, so there is no need to bounce between editor + timer + tracker apps. -
Interruption visibility
You capture not just total work time, but also interruption count and interruption duration. -
Local-first and private
Data stays on your machine: notes in your folders, logs in~/.focusforge/logs/, settings in local app config.
- Upstream PyMarkdownEditor is intentionally editor-first, with plugin support planned for future extensions.
- This fork exists because the focus timer is my primary daily use case as a consultant.
- Rather than waiting for plugin infrastructure, this version ships focus sessions as a core feature.
- So the tool remains immediately useful for real billing and time tracking.
- If you're looking for the canonical "editor-first" implementation, please refer to the upstream project.
High-level repo layout:
. ├── build-requirements.txt ├── CHANGELOG.md ├── config/ ├── CONTRIBUTING.md ├── dev-requirements.txt ├── dist/ ├── docs/ │ ├── CI.md │ ├── RELEASING.md │ ├── focus-timer-preview.gif │ └── screenshot.png ├── LICENSE ├── pyinstaller.spec ├── PyMarkdownEditor.spec ├── pyproject.toml ├── README.md ├── requirements.txt ├── ruff.toml ├── tests/ │ ├── conftest.py │ ├── test_about_dialog.py │ ├── test_container.py │ ├── test_exporter_registry.py │ ├── test_file_service.py │ ├── test_focus_dialogs.py │ ├── test_focus_services.py │ ├── test_html_exporter.py │ ├── test_ini_config_service.py │ ├── test_main_window.py │ ├── test_markdown_renderer.py │ ├── test_models.py │ ├── test_pdf_exporter.py │ ├── test_settings_service.py │ └── test_table_dialog.py └── pymd/ ├── __init__.py ├── __main__.py # python -m pymd entry point ├── main.py # legacy/alt entry point ├── app.py # QApplication bootstrap + DI container wiring ├── di/ │ ├── __init__.py │ └── container.py ├── domain/ │ ├── __init__.py │ ├── interfaces.py │ └── models.py ├── services/ │ ├── __init__.py │ ├── config/ │ │ └── ini_config_service.py │ ├── exporters/ │ │ ├── __init__.py │ │ ├── base.py │ │ ├── html_exporter.py │ │ ├── pdf_exporter.py │ │ └── web_pdf_exporter.py │ ├── focus/ │ │ ├── __init__.py │ │ ├── focus_session_service.py │ │ ├── session_writer.py │ │ └── timer_settings.py │ ├── file_service.py │ ├── markdown_renderer.py │ ├── settings_service.py │ └── ui/ │ ├── __init__.py │ ├── about.py │ ├── create_link.py │ ├── floating_timer_window.py │ ├── find_replace.py │ ├── focus_dialogs.py │ ├── main_window.py │ ├── table_dialog.py │ ├── adapters/ │ │ ├── __init__.py │ │ ├── qt_dialogs.py │ │ ├── qt_messages.py │ │ └── qt_text_editor.py │ ├── commands/ │ │ ├── __init__.py │ │ ├── prefix_lines.py │ │ └── surround_selection.py │ ├── ports/ │ │ ├── __init__.py │ │ ├── dialogs.py │ │ └── messages.py │ └── presenters/ │ ├── __init__.py │ └── main_presenter.py └── utils/ ├── __init__.py └── constants.py
Key pieces:
-
pymd/app.pyApplication bootstrap – sets up Qt app, DI container, and main window. -
pymd/di/container.pyWires services, exporters, and UI into a single container. -
pymd/domain/Core interfaces and models (keeps contracts decoupled from Qt/UI details). -
pymd/services/markdown_renderer.py– wraps Markdown + extensions.file_service.py– safe file IO with atomic writes.settings_service.py/config/ini_config_service.py– settings and INI-based config.focus/– focus session service, note template writer, and timer settings persistence.exporters/– HTML/PDF/Web PDF exporters behind a common base.
-
pymd/services/ui/main_window.py– main editor window.focus_dialogs.py,floating_timer_window.py– pomodoro/session UI.about.py– About dialog.create_link.py,find_replace.py,table_dialog.py– feature dialogs.adapters/– Qt-specific implementations of dialog, message, and text editor ports.ports/– abstraction interfaces for dialogs/messages (for testability and decoupling).presenters/– presenter layer (e.g.main_presenter.py) coordinating UI + services.commands/– small text-editing "command" helpers (e.g. prefix lines, surround selection).
-
pymd/utils/Small shared constants and helpers. -
tests/Coverage for container wiring, dialogs, exporters, config service, and core services.
Install dev dependencies:
pip install -r dev-requirements.txt
Then run the test suite:
pytest --cov=pymd --cov-report=term-missing --timeout=120
dev-requirements.txt includes (excerpt):
pytest>=8.0 pytest-qt>=4.4 pytest-cov>=5.0 pytest-timeout ruff
Notes:
- Qt tests are written to avoid blocking modal dialogs.
- Coverage includes both happy paths and error handling (e.g. file write failures, malformed config).
- A fast "quick CI" config exists for non-
masterbranches, and a fuller matrix runs for PRs tomaster.
You can bundle PyMarkdownEditor into standalone binaries using PyInstaller.
A minimal local example:
pip install pyinstaller pyinstaller -n PyMarkdownEditor --windowed --onefile \ -i NONE -s -y pymd/__main__.py
The repository’s GitHub Actions workflow .github/workflows/release-binaries.yml:
-
Builds on Windows, Linux, and macOS.
-
Uses a
.specfile on Windows for predictable layout. -
Uses platform-appropriate commands on Linux/macOS (e.g.
.appbundle on macOS). -
Collects hidden imports for:
markdownpygmentsPyQt6modulespymdownx(e.g.arithmatex).
Artifacts are zipped or packaged per platform and attached to GitHub Releases for tagged versions (e.g. v0.8.2).
-
Continuous Integration –
.github/workflows/ci.yml- Runs on pushes/PRs.
- Performs
ruffformatting checks andpytestwith coverage. - Uses a quick single-job run for development branches and a full OS/Python matrix for PRs to
master.
-
Binary Releases –
.github/workflows/release-binaries.yml- Triggered by pushing a semver tag:
vMAJOR.MINOR.PATCH(e.g.v0.8.2). - Builds cross-platform binaries and uploads artifacts to the GitHub Release.
- Triggered by pushing a semver tag:
-
PyPI / TestPyPI Publishing –
.github/workflows/publish.yml-
Triggered on tags like
v0.8.0,v0.8.1a1,v0.8.1-foo. -
Verifies that the tag (e.g.
v0.8.2) matchesproject.versioninpyproject.toml. -
Builds sdist + wheel using
python -m build. -
Uses Trusted Publishing:
- Pre-release or hyphenated tags → TestPyPI.
- Final releases → PyPI.
-
See CHANGELOG.md for a detailed history of changes.
-
PDF export blank/empty
- Ensure the target folder exists and is writable.
- Check that any required Qt print/WebEngine libraries are installed.
-
Fonts differ between preview and PDF
- QPrinter rasterization and platform fonts may differ.
- Consider tweaking the CSS or installing appropriate fonts system-wide.
-
Qt / WebEngine errors
- If using the WebEngine PDF exporter, make sure
PyQt6-WebEngineand matching Qt WebEngine libraries are available. - You can fall back to the classic QTextDocument/QPrinter exporter if needed.
- If using the WebEngine PDF exporter, make sure
-
ModuleNotFoundError: 'pymdownx'-
Install
pymdown-extensions:pip install pymdown-extensions
-
Ensure it is present in your environment or packaged with your binary.
-
We welcome issues and pull requests.
- CONTRIBUTING.md – owner-led governance, DCO sign-off, PR checklist.
- LICENSE – Apache-2.0.
.github/CODEOWNERS(if present) – lists current code owners.
"Owner-led governance" means:
- The maintainer steers overall scope and architecture.
- The app aims to stay small, focused, and easy to package.
- PRs are reviewed for fit, clarity, and maintainability before merging.
Quick dev loop:
# format / lint pip install ruff black ruff format . ruff check . # run tests pytest --cov=pymd --cov-report=term-missing --timeout=120
You can wire these into pre-commit hooks for a smoother local workflow.
Apache-2.0 © 2025 clintonshane84
Timesheet fork maintained by SipTech
See LICENSE.