#!/usr/bin/env python3
# Copyright (c) 2022-2025, Anders Andersen, UiT The Arctic University
# of Norway. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
#
# - Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
#
# - Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in
# the documentation and/or other materials provided with the
# distribution.
#
# - Neither the name of the copyright holder nor the names of its
# contributors may be used to endorse or promote products derived
# from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
# COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
R"""Convert source code in the clipboard to document format
Usage: cb-src-to-doc [-h] [-i] [-l LANG] [-f FMT] [--pygmentizecmd PATH]
optional arguments:
-h, --help show this help message and exit
-i run in interactive mode
-l LANG programming language of source code
-f FMT pretty-print document format
--pygmentizecmd PATH the pygmentize command
This program takes the source code (in the programming language
`LANG`) from the clipboard and converts to a pretty-print version in
the `DOC` format (typically LaTeX or HTML). The result is copied back
to the clipboard. The `-i` option makes the program run in interactive
graphical user interface mode.
The program is just a simple wrapper of the `pygmentize` command line
tool [1]. The main motivation is to create a version that is a Mac app
that I can easily run from Spotlight, Alfred or other similar app
launchers. The app is implemented `CB-Src-2-Doc.applescript` program.
[1] https://pygments.org/docs/cmdline/
(c) Copyright 2020–2025 Anders Andersen, UiT The Arctic University of Norway
"""
# -------------------
# Import modules used
# -------------------
# Standard Python modules
import sys, re
# Work with clipboard
import pyperclip
# Use subprocess to perform the command line operations
import subprocess
# -----------------------------
# Handle command line arguments
# -----------------------------
# Create argument parser
import argparse
parser = argparse.ArgumentParser(
description="Convert source code in the clipboard to document format")
parser.add_argument(
"-i", action="store_true", default=False,
help="run in interactive mode")
parser.add_argument(
"-l", default="py3", help="programming language of source code")
parser.add_argument(
"-f", default="html", help="pretty-print document format")
parser.add_argument(
"--pygmentizecmd", default="/usr/local/bin/pygmentize",
help="the pygmentize command")
# Parse the arguemnts
args = parser.parse_args()
# No errors so far
errmsg = ""
# ------------
# The GUI part
# ------------
# Interactive (GUI) mode selected
if args.i:
from PySide6.QtWidgets import QApplication, QDialog
from PySide6.QtWidgets import QHBoxLayout, QVBoxLayout
from PySide6.QtWidgets import QPushButton, QLabel, QLineEdit
class SrcDocDialog(QDialog):
def __init__(self, app, args, lexers, formatters):
super(SrcDocDialog, self).__init__()
self.setWindowTitle("Convert src in clipboard to pp format")
self.app = app
self.args = args
self.lexers = lexers
self.formatters = formatters
self.choose = QHBoxLayout()
self.buttons = QHBoxLayout()
self.layout = QVBoxLayout()
self.label = QLabel("Choose lexer and formatter:")
self.layout.addWidget(self.label)
self.lex = QLineEdit(args.l)
self.lex.textChanged.connect(self.check_ok)
self.choose.addWidget(self.lex)
self.fmt = QLineEdit(args.f)
self.fmt.textChanged.connect(self.check_ok)
self.choose.addWidget(self.fmt)
self.layout.addLayout(self.choose)
self.button_cancel = QPushButton("Cancel")
self.button_cancel.setMinimumWidth(85)
self.buttons.addWidget(self.button_cancel)
self.button_ok = QPushButton("OK")
self.button_ok.setMinimumWidth(85)
self.buttons.addWidget(self.button_ok)
self.button_ok.setDefault(True)
self.button_ok.setEnabled(False)
self.layout.addLayout(self.buttons)
self.button_cancel.clicked.connect(self.cancel)
self.button_ok.clicked.connect(self.ok)
self.setLayout(self.layout)
self.check_ok()
def check_ok(self):
lex = self.lex.text()
fmt = self.fmt.text()
if (lex in self.lexers) and (fmt in self.formatters):
self.button_ok.setEnabled(True)
else:
self.button_ok.setEnabled(False)
def ok(self):
self.args.l = self.lex.text()
self.args.f = self.fmt.text()
self.app.closeAllWindows()
def cancel(self):
self.args.l = None
self.app.closeAllWindows()
def do_srcdoc_dialog(args, lexers, formatters):
app = QApplication()
dialog = SrcDocDialog(app, args, lexers, formatters)
dialog.show()
return app, app.exec()
# ------------------------------------------------
# Interact with the `pygmentize` command line tool
# ------------------------------------------------
# Regular expression to get the lexers/formatters
mexp = re.compile(r"\* ((\w|-|\+|,| )+):", re.ASCII)
def get_available(which):
"""Function to get lexers or formatters
This function returns either the avalable lexers or the available
formatters (the `which` argument specify which) of the
`pygmentize` command line tool.
"""
# Perform the list command of `pygmentize`
res = subprocess.run(
[args.pygmentizecmd, "-L", which],
text=True, capture_output=True)
res.check_returncode()
# Parse the output of the list command
lst = []
for line in res.stdout.splitlines():
m = mexp.match(line)
if m:
lst += m.group(1).split(", ")
# Return the list
return lst
# Do the `pygmentize` commands
try:
# Get all available lexers (programming languages) and formatters
lexers = get_available("lexer")
formatters = get_available("formatter")
# Interactive GUI mode?
if args.i:
try:
app, status = do_srcdoc_dialog(args, lexers, formatters)
except Exception as err:
errmsg = str(err)
# User selected cancel
if args.l == None:
sys.exit(0)
# Ignore if previous steps failed
if not errmsg:
# Check if lexers and formatters are available
if not args.l in lexers:
errmsg += "{} is not a valid pygmentize lexer".format(args.l)
if not args.f in formatters:
errmsg += "{} is not a valid pygmentize formatter".format(args.f)
# Perform the `pygmentize` command (input from clipboard)
if not errmsg:
res = subprocess.run(
[args.pygmentizecmd, "-l", args.l, "-f", args.f],
input=pyperclip.paste(), text=True, capture_output=True)
res.check_returncode()
# Something went wrong
except subprocess.CalledProcessError as err:
errmsg = err.stderr
# ----------------------
# Did anything go wrong?
# ----------------------
if errmsg:
if args.i:
class StatusDialog(QDialog):
def __init__(self, app, msg):
super(StatusDialog, self).__init__()
self.app = app
self.setWindowTitle("Error")
self.text = QPlainTextEdit()
self.text.setPlaceholderText(msg)
self.text.setReadOnly(True)
self.button_ok = QPushButton("OK")
self.button_ok.clicked.connect(self.ok)
self.layout = QVBoxLayout()
self.layout.addWidget(self.text)
self.layout.addWidget(self.button_ok)
self.setLayout(self.layout)
def ok(self):
self.app.closeAllWindows()
dialog = StatusDialog(app, errmsg)
dialog.show()
status = app.exec()
sys.exit(1)
else:
print(errmsg, file=sys.stderr)
sys.exit(1)
# -----------------------------
# Copy result back to clipboard
# -----------------------------
pyperclip.copy(res.stdout.rstrip())