mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-22 09:34:53 +02:00
* [eric] ci: gitleaks-ignore the known historical secrets so our branch stops failing on leaks it didnt add * [eric] workflows: restore scheduled-tasks on the workflow line (revert removal, keep windows fixes + 1.1.69) * [eric] workflows: re-apply uncommitted scheduling wip (schedule pill, calendar view, slice) * [eric] ops: gitignore dev-team local state files * [eric] ops: backlog item for download-tracking visibility * [eric] ci: allowlist the cdp-routes redaction-test token in gitleaks * [aidan] feat/scheduled-tasks: keep step labels in sync on edit and show chevron on every step * [aidan] fix: schedule time in chat * [aidan] ux/workflows: add workflow step removal (#91) * [aidan] feat/scheduled-tasks: remember workflow tool permissions across runs * [aidan] feat/task-scheduling: add hourly and minute (15-min minimum) schedule intervals (#93) * [aidan] feat/scheduled-tasks: calendar, rename, and edit workflows (#94) * [aidan] bug: fix schedule button * [aidan] fix/agent-errors: surface provider rate limits * [aidan] ux/cards: click-to-rename for chat and workflow titles Single-click a card's title to enter edit mode inline. Commit on Enter/blur, cancel on Escape. Rename persists via PATCH for workflows and sessions. * [aidan] feat/workflows: seed build prompt for zero-step workflows When a new workflow has no steps, seed the agent with a prompt asking the user to describe what the workflow should do, rather than starting blank. * [aidan] feat/workflows: add-to-schedule popover for unscheduled workflows Clicking the "+" on an unscheduled workflow row opens a popover with two options: - Keep this schedule: enables the workflow's existing cadence and moves it to Scheduled - Change schedule: opens the scheduling editor to pick a different time * [aidan] ux/workflows: wire add-to-schedule popover and simplify New button - Made the "+" icon on unscheduled workflow rows clickable, opening a popover to keep or change the schedule - Removed AddIcon from toolbar "New" button (now reads "New" instead of "+ New") * [aidan] fix/scheduled-tasks: open schedule calendar when Schedule pill clicked Fixed the Schedule pill click being swallowed by the toolbar's dismiss handler. Exempted the toolbar pills via data-toolbar-pills so their click handlers fire. * [aidan] ux/workflows: open New workflow in agent build chat instead of empty card When creating a new workflow from the hub, open it in edit_agent view (with the agent builder chat) instead of a preview card. The workflow is created on the backend first so the embedded session has a real ID. * [aidan] feat/workflow-edit: add draft testing save flow * [aidan] ux/chat: remove continue chat button * [aidan] ux/workflows: polish workflow card interactions * [aidan] fix/workflow-scheduling: save unscheduled workflows as drafts * aidan ui: schedule naming changes * [aidan] ui: tool calling desc/naming * [aidan] ui: calendar sidebar naming * [aidan] ui: fix stop viewing closing chat * [aidan] feat/workflows: auto-name workflows and polish the build flow (#95) * [aidan] feat/workflow-auto-naming: auto-generate workflow titles from steps Generate a title + description from a workflow's steps (one aux call, reused for step labels) whenever it is still auto_named, so a workflow built in the Edit Agent names itself on commit instead of staying "New workflow". A manual rename sets auto_named=False and is never overwritten. Stream the aux call (non-streaming drops content on some 9router lanes) and fall back to a step-derived title when the model is unavailable. * [aidan] feat/workflows: hide unsaved new workflows until first save A brand-new "+ New" workflow is created with unsaved=true and kept out of the hub's scheduled/unscheduled lists while the user is still building it in the Edit Agent. The first commit (Save) clears the flag and the workflow appears. Every other create path stays visible immediately. * [aidan] ux/workflows: remove redundant save workflow button The Edit Agent already has Discard/Save controls in its strip, so the header "Save Workflow" button was a duplicate save path. Remove it and its pulse/edit-session-id wiring; the model/time subtitle stays. * [aidan] ux/workflows: animate title on auto-rename Wrap the workflow card title in the same Typewriter the chat card uses, so when the auto-generated name replaces the placeholder after Save it retypes letter-by-letter. Gated on a real (non-placeholder) title so it never animates on mount or for already-named workflows. * [aidan] ux/workflows: animate sidebar title on auto-rename Wrap the calendar hub's sidebar row title in the same Typewriter the workflow card uses, so a title that auto-renames retypes letter-by-letter in the sidebar too. Extract the placeholder/isRealTitle guard into the shared workflowVisuals so the card and sidebar stay in sync. * [aidan] fix/workflows: connect watch tether, keep watched chat open, wire draft run/history * [aidan] ui: grey out chat pill when not selected * [aidan] ui: fix running agent display * [aidan] feat/history-popover: add chat history and scheduled tasks run log tabs (#96) * [aidan] ux/schedule: toast when calendar view already open on expand * [aidan] ui: fix popover descs * [aidan] feat/workflow-runs: add pause, resume, and stop controls for live runs * [aidan] feat/schedule-calendar: add calendar occurrences endpoint and concrete timezones * [aidan] feat/workflows: require at least one step to save a workflow * [aidan] ux/edit-agent: hide Discard for an unsaved new workflow * [aidan] ux/edit-agent: move fix-prefix card below the step list * [aidan] feat/workflows: toast when an unattended scheduled run starts * [aidan] fix/dashboard-tethers: anchor workflow-sidecar tethers to measured card rects * [aidan] feat/workflows: validate steps before scheduling and keep chat tool memory * [aidan] feat/mcp-suggestions: dismissable integration banner with per-session cooldown * [aidan] feat/workflows: add scheduled-run "running now" toast with click-to-view (#97) * [aidan] ux/workflows: surface paused state on card, sidebar, and calendar; tidy run history * [aidan] feat/mcp-suggestions: suggest both Google and Microsoft when provider is ambiguous * [aidan] fix/agent-tokens: friendly out-of-tokens card across all agent surfaces * [aidan] feat/workflow-model: persist edit-agent model on save with switch notice and fresh drafts * [aidan] fix/workflow-chat: force stop on watched run mirrors workflow card stop * [aidan] fix/workflow-cards: keep watched run tethered on finish to avoid duplicate chat * [aidan] feat/schedule-calendar: mark current time with a now line in week view * [aidan] refactor/private-names: rename error and schedule classifiers from _ to p_ * [aidan] feat/scheduled-tasks: agent workflow scheduling and in-chat convert (#98) * [aidan] feat/workflow-suggest: nudge user to convert repeatable chat to workflow Add SuggestConvertToWorkflow MCP tool that agents call at the end of a task when they've completed something worth repeating (daily report, weekly check, recurring data pull). Frontend detects the tool call and glows the "Convert to workflow" button 3 times to draw the eye. When user clicks it, the suggested cadence (e.g. "every weekday at 9am") is stored in the draft and seeded into the scheduling agent's first prompt, so the agent can act on the suggestion rather than asking the user again. Tool is never auto-called — agents decide when a task is genuinely repeatable (not debugging, creative work, one-off lookup). Tool description emphasizes sparse, high-confidence use only (once per session max). Files changed: - backend/apps/agents/schedule_mcp_server.py: add SuggestConvertToWorkflow tool - frontend/src/shared/mcpToolMeta.ts: add label for new tool - frontend/src/app/pages/Dashboard/cards/AgentCard.tsx: detect suggestion in session messages, show+glow "Convert to workflow" button, pass cadence to draft - frontend/src/shared/state/workflowsSlice.ts: add suggested_cadence field to Workflow interface - frontend/src/app/pages/Workflows/SchedulingView.tsx: seed scheduling agent prompt with suggested cadence hint * [aidan] feat/agent-scheduling: route recurring asks through native workflows, deny claude cron skill * [aidan] feat/workflow-convert: in-chat convert popup and auto-open scheduled workflow card * [aidan] ux/calendar-page: schedule calendar restyle + popover fixes (#99) * [aidan] fix/dashboard-delete: remove workflows calendar panel on delete key * [aidan] ux/workflows-calendar: restyle hub, fix today highlight, add toolbar toggle * [aidan] ux/schedule-popover: compact density, fix sticky header bleed, add header spacing * [aidan] ux/schedule-calendar: hollow ring dot for past fires in month view * [aidan] ux/schedule-calendar: clickable +N more opens day's full run list * [aidan] feat/run-log-filters: add success and skipped pills to scheduled task history * [aidan] fix/convert-button: stop drag capture so convert-to-workflow click fires * [aidan] ux/calendar-card: match border color and radius to chat and workflow cards * [aidan] fix/minimap: render missed-runs card on the minimap * [aidan] ux/run-sparkline: simplify tooltip to plain run tally * [aidan] ux/run-history: collapse expanded run view to one clickable line * [aidan] ux/calendar-card: match corner radius to browser cards * [aidan] feat/workflows: launch-time scheduling UX and workflow-card polish (#101) * [aidan] feat/schedule-list: lazy-load list view via scroll sentinel * [aidan] feat/missed-runs: launch toast with per-workflow counts and pan-to-card * [aidan] fix/dashboard-tethers: keep watching line anchored on canvas zoom * [aidan] feat/scheduled-tasks: review missed runs at launch instead of auto-firing on_missed * [aidan] refactor/workflow-cards: use radius and status design tokens, polish card chrome * [aidan] ux/agent-card: keep convert-to-workflow visible during runs with mid-turn toast * [aidan] ux/mcp-bubble: drop redundant verb label when a workflow label is shown * [aidan] chore/backend: remove stale explanatory comments * [aidan] fix/workflows-hub: load workflows on hub mount so calendar fills at launch * [aidan] feat/workflows: generate title, description, step labels at convert time * [aidan] fix/tidy-layout: include workflows hub in tidy and fit-to-view * [aidan] feat/schedule-list: window long list via measured-height virtualizer * [aidan] ux/workflows-hub: remove time-saved badge from calendar header * [aidan] fix/types: add missing semantic-type labels and drop stray fade arg * [aidan] feat/schedule: pin monthly day-of-month and honor repeat-every intervals * [aidan] feat/schedule: inherit source-session tool surface for scheduled runs * [aidan] ux/calendar: restack hour-cell events as bars with overflow affordance * [aidan] feat/calendar: open the run card when clicking a scheduled occurrence * [aidan] ux/missed-runs: add per-group select-all toggle and rename skip action * [aidan] feat: new scheduled task design ported * [aidan] ui: sidebar reorder, repeat controls on schedule card * [aidan] ui: sidebar, scheduling time * [aidan] feat/schedule: pin monthly last-day-of-month * [aidan] feat/steps: per-step enable toggle * [aidan] feat/workflows: per-workflow color swatch * [aidan] feat/trash: soft-delete workflows with restore and purge * [aidan] feat/run-monitor: live run monitor card on the canvas * [aidan] feat/run-context: attach a run as removable chat context * [aidan] feat/compose: new-workflow landing page and auto-commit build flow * [aidan] ui/workflows: dark mode and design-system cohesion * [aidan] ui/calendar: overflow popover, condensed week view, scroll fix * [aidan] feat/home: ongoing runs, missed review, and accurate Coming-up counts * [aidan] fix/run-status: sync ongoing runs and heal stuck/interrupted runs * [aidan] ux/schedule: last-day-of-month UI, Run-at time typing, interval input * [aidan] ux/workflows: default window size and toolbar icon * [aidan] fix/schedule: measure ran_late from start and anchor recurrences to created_at * [aidan] feat/calendar: render fire times from backend, drop JS recurrence reimpl * [aidan] chore/dashboard: drop dead configure/missed-run cards, refetch on reconnect * [aidan] chore/agent-card: remove unreachable convert-to-workflow action * [aidan] fix/workflows: don't bump updated_at on a no-op draft commit so viewing a workflow doesn't reorder the sidebar * [aidan] feat/schedule: warn when scheduling a workflow that has no steps * [aidan] fix/selection-tool: never select the workflows app, and exit the tool on Escape without dropping selections * [aidan] ux/compose: diversify new-workflow starter prompts across personas * [aidan] ux/run-monitor: spawn the run card a bit farther right of the workflows app * [aidan] fix/schedule: harden run recovery and storage writes against crashes * [aidan] ux/compose: restyle new-workflow starters as a clean pill cluster with rich prompts * [aidan] fix/workflows: optimistically apply edits so the schedule banner updates instantly * [aidan] ui/workflows: three-tone surface depth so the window lifts off the canvas in both themes * [aidan] ui/workflows: close buttons turn red on hover, matching the chat card * [aidan] test/schedule: cover executor pipeline, storage durability, and recurrence gaps * [aidan] fix: remove package-lock json * [aidan] fix/workflows-compose: keep compose view until edit agent replies * [aidan] feat/workflows: auto-generate workflow + step titles with typewriter animation * [eric] deps: restore frontend/package-lock.json (PR #105 deletion broke npm ci) --------- Co-authored-by: Eric <ciregenz@berkeley.edu> Co-authored-by: cire <134991075+ciregenz@users.noreply.github.com>
669 lines
23 KiB
Python
669 lines
23 KiB
Python
"""
|
|
pygments.cmdline
|
|
~~~~~~~~~~~~~~~~
|
|
|
|
Command line interface.
|
|
|
|
:copyright: Copyright 2006-present by the Pygments team, see AUTHORS.
|
|
:license: BSD, see LICENSE for details.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import shutil
|
|
import argparse
|
|
from textwrap import dedent
|
|
|
|
from pygments import __version__, highlight
|
|
from pygments.util import ClassNotFound, OptionError, docstring_headline, \
|
|
guess_decode, guess_decode_from_terminal, terminal_encoding, \
|
|
UnclosingTextIOWrapper
|
|
from pygments.lexers import get_all_lexers, get_lexer_by_name, guess_lexer, \
|
|
load_lexer_from_file, get_lexer_for_filename, find_lexer_class_for_filename
|
|
from pygments.lexers.special import TextLexer
|
|
from pygments.formatters.latex import LatexEmbeddedLexer, LatexFormatter
|
|
from pygments.formatters import get_all_formatters, get_formatter_by_name, \
|
|
load_formatter_from_file, get_formatter_for_filename, find_formatter_class
|
|
from pygments.formatters.terminal import TerminalFormatter
|
|
from pygments.formatters.terminal256 import Terminal256Formatter, TerminalTrueColorFormatter
|
|
from pygments.filters import get_all_filters, find_filter_class
|
|
from pygments.styles import get_all_styles, get_style_by_name
|
|
|
|
|
|
def _parse_options(o_strs):
|
|
opts = {}
|
|
if not o_strs:
|
|
return opts
|
|
for o_str in o_strs:
|
|
if not o_str.strip():
|
|
continue
|
|
o_args = o_str.split(',')
|
|
for o_arg in o_args:
|
|
o_arg = o_arg.strip()
|
|
try:
|
|
o_key, o_val = o_arg.split('=', 1)
|
|
o_key = o_key.strip()
|
|
o_val = o_val.strip()
|
|
except ValueError:
|
|
opts[o_arg] = True
|
|
else:
|
|
opts[o_key] = o_val
|
|
return opts
|
|
|
|
|
|
def _parse_filters(f_strs):
|
|
filters = []
|
|
if not f_strs:
|
|
return filters
|
|
for f_str in f_strs:
|
|
if ':' in f_str:
|
|
fname, fopts = f_str.split(':', 1)
|
|
filters.append((fname, _parse_options([fopts])))
|
|
else:
|
|
filters.append((f_str, {}))
|
|
return filters
|
|
|
|
|
|
def _print_help(what, name):
|
|
try:
|
|
if what == 'lexer':
|
|
cls = get_lexer_by_name(name)
|
|
print(f"Help on the {cls.name} lexer:")
|
|
print(dedent(cls.__doc__))
|
|
elif what == 'formatter':
|
|
cls = find_formatter_class(name)
|
|
print(f"Help on the {cls.name} formatter:")
|
|
print(dedent(cls.__doc__))
|
|
elif what == 'filter':
|
|
cls = find_filter_class(name)
|
|
print(f"Help on the {name} filter:")
|
|
print(dedent(cls.__doc__))
|
|
return 0
|
|
except (AttributeError, ValueError):
|
|
print(f"{what} not found!", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
def _print_list(what):
|
|
if what == 'lexer':
|
|
print()
|
|
print("Lexers:")
|
|
print("~~~~~~~")
|
|
|
|
info = []
|
|
for fullname, names, exts, _ in get_all_lexers():
|
|
tup = (', '.join(names)+':', fullname,
|
|
exts and '(filenames ' + ', '.join(exts) + ')' or '')
|
|
info.append(tup)
|
|
info.sort()
|
|
for i in info:
|
|
print(('* {}\n {} {}').format(*i))
|
|
|
|
elif what == 'formatter':
|
|
print()
|
|
print("Formatters:")
|
|
print("~~~~~~~~~~~")
|
|
|
|
info = []
|
|
for cls in get_all_formatters():
|
|
doc = docstring_headline(cls)
|
|
tup = (', '.join(cls.aliases) + ':', doc, cls.filenames and
|
|
'(filenames ' + ', '.join(cls.filenames) + ')' or '')
|
|
info.append(tup)
|
|
info.sort()
|
|
for i in info:
|
|
print(('* {}\n {} {}').format(*i))
|
|
|
|
elif what == 'filter':
|
|
print()
|
|
print("Filters:")
|
|
print("~~~~~~~~")
|
|
|
|
for name in get_all_filters():
|
|
cls = find_filter_class(name)
|
|
print("* " + name + ':')
|
|
print(f" {docstring_headline(cls)}")
|
|
|
|
elif what == 'style':
|
|
print()
|
|
print("Styles:")
|
|
print("~~~~~~~")
|
|
|
|
for name in get_all_styles():
|
|
cls = get_style_by_name(name)
|
|
print("* " + name + ':')
|
|
print(f" {docstring_headline(cls)}")
|
|
|
|
|
|
def _print_list_as_json(requested_items):
|
|
import json
|
|
result = {}
|
|
if 'lexer' in requested_items:
|
|
info = {}
|
|
for fullname, names, filenames, mimetypes in get_all_lexers():
|
|
info[fullname] = {
|
|
'aliases': names,
|
|
'filenames': filenames,
|
|
'mimetypes': mimetypes
|
|
}
|
|
result['lexers'] = info
|
|
|
|
if 'formatter' in requested_items:
|
|
info = {}
|
|
for cls in get_all_formatters():
|
|
doc = docstring_headline(cls)
|
|
info[cls.name] = {
|
|
'aliases': cls.aliases,
|
|
'filenames': cls.filenames,
|
|
'doc': doc
|
|
}
|
|
result['formatters'] = info
|
|
|
|
if 'filter' in requested_items:
|
|
info = {}
|
|
for name in get_all_filters():
|
|
cls = find_filter_class(name)
|
|
info[name] = {
|
|
'doc': docstring_headline(cls)
|
|
}
|
|
result['filters'] = info
|
|
|
|
if 'style' in requested_items:
|
|
info = {}
|
|
for name in get_all_styles():
|
|
cls = get_style_by_name(name)
|
|
info[name] = {
|
|
'doc': docstring_headline(cls)
|
|
}
|
|
result['styles'] = info
|
|
|
|
json.dump(result, sys.stdout)
|
|
|
|
def main_inner(parser, argns):
|
|
if argns.help:
|
|
parser.print_help()
|
|
return 0
|
|
|
|
if argns.V:
|
|
print(f'Pygments version {__version__}, (c) 2006-present by Georg Brandl, Matthäus '
|
|
'Chajdas and contributors.')
|
|
return 0
|
|
|
|
def is_only_option(opt):
|
|
return not any(v for (k, v) in vars(argns).items() if k != opt)
|
|
|
|
# handle ``pygmentize -L``
|
|
if argns.L is not None:
|
|
arg_set = set()
|
|
for k, v in vars(argns).items():
|
|
if v:
|
|
arg_set.add(k)
|
|
|
|
arg_set.discard('L')
|
|
arg_set.discard('json')
|
|
|
|
if arg_set:
|
|
parser.print_help(sys.stderr)
|
|
return 2
|
|
|
|
# print version
|
|
if not argns.json:
|
|
main(['', '-V'])
|
|
allowed_types = {'lexer', 'formatter', 'filter', 'style'}
|
|
largs = [arg.rstrip('s') for arg in argns.L]
|
|
if any(arg not in allowed_types for arg in largs):
|
|
parser.print_help(sys.stderr)
|
|
return 0
|
|
if not largs:
|
|
largs = allowed_types
|
|
if not argns.json:
|
|
for arg in largs:
|
|
_print_list(arg)
|
|
else:
|
|
_print_list_as_json(largs)
|
|
return 0
|
|
|
|
# handle ``pygmentize -H``
|
|
if argns.H:
|
|
if not is_only_option('H'):
|
|
parser.print_help(sys.stderr)
|
|
return 2
|
|
what, name = argns.H
|
|
if what not in ('lexer', 'formatter', 'filter'):
|
|
parser.print_help(sys.stderr)
|
|
return 2
|
|
return _print_help(what, name)
|
|
|
|
# parse -O options
|
|
parsed_opts = _parse_options(argns.O or [])
|
|
|
|
# parse -P options
|
|
for p_opt in argns.P or []:
|
|
try:
|
|
name, value = p_opt.split('=', 1)
|
|
except ValueError:
|
|
parsed_opts[p_opt] = True
|
|
else:
|
|
parsed_opts[name] = value
|
|
|
|
# encodings
|
|
inencoding = parsed_opts.get('inencoding', parsed_opts.get('encoding'))
|
|
outencoding = parsed_opts.get('outencoding', parsed_opts.get('encoding'))
|
|
|
|
# handle ``pygmentize -N``
|
|
if argns.N:
|
|
lexer = find_lexer_class_for_filename(argns.N)
|
|
if lexer is None:
|
|
lexer = TextLexer
|
|
|
|
print(lexer.aliases[0])
|
|
return 0
|
|
|
|
# handle ``pygmentize -C``
|
|
if argns.C:
|
|
inp = sys.stdin.buffer.read()
|
|
try:
|
|
lexer = guess_lexer(inp, inencoding=inencoding)
|
|
except ClassNotFound:
|
|
lexer = TextLexer
|
|
|
|
print(lexer.aliases[0])
|
|
return 0
|
|
|
|
# handle ``pygmentize -S``
|
|
S_opt = argns.S
|
|
a_opt = argns.a
|
|
if S_opt is not None:
|
|
f_opt = argns.f
|
|
if not f_opt:
|
|
parser.print_help(sys.stderr)
|
|
return 2
|
|
if argns.l or argns.INPUTFILE:
|
|
parser.print_help(sys.stderr)
|
|
return 2
|
|
|
|
try:
|
|
parsed_opts['style'] = S_opt
|
|
fmter = get_formatter_by_name(f_opt, **parsed_opts)
|
|
except ClassNotFound as err:
|
|
print(err, file=sys.stderr)
|
|
return 1
|
|
|
|
print(fmter.get_style_defs(a_opt or ''))
|
|
return 0
|
|
|
|
# if no -S is given, -a is not allowed
|
|
if argns.a is not None:
|
|
parser.print_help(sys.stderr)
|
|
return 2
|
|
|
|
# parse -F options
|
|
F_opts = _parse_filters(argns.F or [])
|
|
|
|
# -x: allow custom (eXternal) lexers and formatters
|
|
allow_custom_lexer_formatter = bool(argns.x)
|
|
|
|
# select lexer
|
|
lexer = None
|
|
|
|
# given by name?
|
|
lexername = argns.l
|
|
if lexername:
|
|
# custom lexer, located relative to user's cwd
|
|
if allow_custom_lexer_formatter and '.py' in lexername:
|
|
try:
|
|
filename = None
|
|
name = None
|
|
if ':' in lexername:
|
|
filename, name = lexername.rsplit(':', 1)
|
|
|
|
if '.py' in name:
|
|
# This can happen on Windows: If the lexername is
|
|
# C:\lexer.py -- return to normal load path in that case
|
|
name = None
|
|
|
|
if filename and name:
|
|
lexer = load_lexer_from_file(filename, name,
|
|
**parsed_opts)
|
|
else:
|
|
lexer = load_lexer_from_file(lexername, **parsed_opts)
|
|
except ClassNotFound as err:
|
|
print('Error:', err, file=sys.stderr)
|
|
return 1
|
|
else:
|
|
try:
|
|
lexer = get_lexer_by_name(lexername, **parsed_opts)
|
|
except (OptionError, ClassNotFound) as err:
|
|
print('Error:', err, file=sys.stderr)
|
|
return 1
|
|
|
|
# read input code
|
|
code = None
|
|
|
|
if argns.INPUTFILE:
|
|
if argns.s:
|
|
print('Error: -s option not usable when input file specified',
|
|
file=sys.stderr)
|
|
return 2
|
|
|
|
infn = argns.INPUTFILE
|
|
try:
|
|
with open(infn, 'rb') as infp:
|
|
code = infp.read()
|
|
except Exception as err:
|
|
print('Error: cannot read infile:', err, file=sys.stderr)
|
|
return 1
|
|
if not inencoding:
|
|
code, inencoding = guess_decode(code)
|
|
|
|
# do we have to guess the lexer?
|
|
if not lexer:
|
|
try:
|
|
lexer = get_lexer_for_filename(infn, code, **parsed_opts)
|
|
except ClassNotFound as err:
|
|
if argns.g:
|
|
try:
|
|
lexer = guess_lexer(code, **parsed_opts)
|
|
except ClassNotFound:
|
|
lexer = TextLexer(**parsed_opts)
|
|
else:
|
|
print('Error:', err, file=sys.stderr)
|
|
return 1
|
|
except OptionError as err:
|
|
print('Error:', err, file=sys.stderr)
|
|
return 1
|
|
|
|
elif not argns.s: # treat stdin as full file (-s support is later)
|
|
# read code from terminal, always in binary mode since we want to
|
|
# decode ourselves and be tolerant with it
|
|
code = sys.stdin.buffer.read() # use .buffer to get a binary stream
|
|
if not inencoding:
|
|
code, inencoding = guess_decode_from_terminal(code, sys.stdin)
|
|
# else the lexer will do the decoding
|
|
if not lexer:
|
|
try:
|
|
lexer = guess_lexer(code, **parsed_opts)
|
|
except ClassNotFound:
|
|
lexer = TextLexer(**parsed_opts)
|
|
|
|
else: # -s option needs a lexer with -l
|
|
if not lexer:
|
|
print('Error: when using -s a lexer has to be selected with -l',
|
|
file=sys.stderr)
|
|
return 2
|
|
|
|
# process filters
|
|
for fname, fopts in F_opts:
|
|
try:
|
|
lexer.add_filter(fname, **fopts)
|
|
except ClassNotFound as err:
|
|
print('Error:', err, file=sys.stderr)
|
|
return 1
|
|
|
|
# select formatter
|
|
outfn = argns.o
|
|
fmter = argns.f
|
|
if fmter:
|
|
# custom formatter, located relative to user's cwd
|
|
if allow_custom_lexer_formatter and '.py' in fmter:
|
|
try:
|
|
filename = None
|
|
name = None
|
|
if ':' in fmter:
|
|
# Same logic as above for custom lexer
|
|
filename, name = fmter.rsplit(':', 1)
|
|
|
|
if '.py' in name:
|
|
name = None
|
|
|
|
if filename and name:
|
|
fmter = load_formatter_from_file(filename, name,
|
|
**parsed_opts)
|
|
else:
|
|
fmter = load_formatter_from_file(fmter, **parsed_opts)
|
|
except ClassNotFound as err:
|
|
print('Error:', err, file=sys.stderr)
|
|
return 1
|
|
else:
|
|
try:
|
|
fmter = get_formatter_by_name(fmter, **parsed_opts)
|
|
except (OptionError, ClassNotFound) as err:
|
|
print('Error:', err, file=sys.stderr)
|
|
return 1
|
|
|
|
if outfn:
|
|
if not fmter:
|
|
try:
|
|
fmter = get_formatter_for_filename(outfn, **parsed_opts)
|
|
except (OptionError, ClassNotFound) as err:
|
|
print('Error:', err, file=sys.stderr)
|
|
return 1
|
|
try:
|
|
outfile = open(outfn, 'wb')
|
|
except Exception as err:
|
|
print('Error: cannot open outfile:', err, file=sys.stderr)
|
|
return 1
|
|
else:
|
|
if not fmter:
|
|
if os.environ.get('COLORTERM','') in ('truecolor', '24bit'):
|
|
fmter = TerminalTrueColorFormatter(**parsed_opts)
|
|
elif '256' in os.environ.get('TERM', ''):
|
|
fmter = Terminal256Formatter(**parsed_opts)
|
|
else:
|
|
fmter = TerminalFormatter(**parsed_opts)
|
|
outfile = sys.stdout.buffer
|
|
|
|
# determine output encoding if not explicitly selected
|
|
if not outencoding:
|
|
if outfn:
|
|
# output file? use lexer encoding for now (can still be None)
|
|
fmter.encoding = inencoding
|
|
else:
|
|
# else use terminal encoding
|
|
fmter.encoding = terminal_encoding(sys.stdout)
|
|
|
|
# provide coloring under Windows, if possible
|
|
if not outfn and sys.platform in ('win32', 'cygwin') and \
|
|
fmter.name in ('Terminal', 'Terminal256'): # pragma: no cover
|
|
# unfortunately colorama doesn't support binary streams on Py3
|
|
outfile = UnclosingTextIOWrapper(outfile, encoding=fmter.encoding)
|
|
fmter.encoding = None
|
|
try:
|
|
import colorama.initialise
|
|
except ImportError:
|
|
pass
|
|
else:
|
|
outfile = colorama.initialise.wrap_stream(
|
|
outfile, convert=None, strip=None, autoreset=False, wrap=True)
|
|
|
|
# When using the LaTeX formatter and the option `escapeinside` is
|
|
# specified, we need a special lexer which collects escaped text
|
|
# before running the chosen language lexer.
|
|
escapeinside = parsed_opts.get('escapeinside', '')
|
|
if len(escapeinside) == 2 and isinstance(fmter, LatexFormatter):
|
|
left = escapeinside[0]
|
|
right = escapeinside[1]
|
|
lexer = LatexEmbeddedLexer(left, right, lexer)
|
|
|
|
# ... and do it!
|
|
if not argns.s:
|
|
# process whole input as per normal...
|
|
try:
|
|
highlight(code, lexer, fmter, outfile)
|
|
finally:
|
|
if outfn:
|
|
outfile.close()
|
|
return 0
|
|
else:
|
|
# line by line processing of stdin (eg: for 'tail -f')...
|
|
try:
|
|
while 1:
|
|
line = sys.stdin.buffer.readline()
|
|
if not line:
|
|
break
|
|
if not inencoding:
|
|
line = guess_decode_from_terminal(line, sys.stdin)[0]
|
|
highlight(line, lexer, fmter, outfile)
|
|
if hasattr(outfile, 'flush'):
|
|
outfile.flush()
|
|
return 0
|
|
except KeyboardInterrupt: # pragma: no cover
|
|
return 0
|
|
finally:
|
|
if outfn:
|
|
outfile.close()
|
|
|
|
|
|
class HelpFormatter(argparse.HelpFormatter):
|
|
def __init__(self, prog, indent_increment=2, max_help_position=16, width=None):
|
|
if width is None:
|
|
try:
|
|
width = shutil.get_terminal_size().columns - 2
|
|
except Exception:
|
|
pass
|
|
argparse.HelpFormatter.__init__(self, prog, indent_increment,
|
|
max_help_position, width)
|
|
|
|
|
|
def main(args=sys.argv):
|
|
"""
|
|
Main command line entry point.
|
|
"""
|
|
desc = "Highlight an input file and write the result to an output file."
|
|
parser = argparse.ArgumentParser(description=desc, add_help=False,
|
|
formatter_class=HelpFormatter)
|
|
|
|
operation = parser.add_argument_group('Main operation')
|
|
lexersel = operation.add_mutually_exclusive_group()
|
|
lexersel.add_argument(
|
|
'-l', metavar='LEXER',
|
|
help='Specify the lexer to use. (Query names with -L.) If not '
|
|
'given and -g is not present, the lexer is guessed from the filename.')
|
|
lexersel.add_argument(
|
|
'-g', action='store_true',
|
|
help='Guess the lexer from the file contents, or pass through '
|
|
'as plain text if nothing can be guessed.')
|
|
operation.add_argument(
|
|
'-F', metavar='FILTER[:options]', action='append',
|
|
help='Add a filter to the token stream. (Query names with -L.) '
|
|
'Filter options are given after a colon if necessary.')
|
|
operation.add_argument(
|
|
'-f', metavar='FORMATTER',
|
|
help='Specify the formatter to use. (Query names with -L.) '
|
|
'If not given, the formatter is guessed from the output filename, '
|
|
'and defaults to the terminal formatter if the output is to the '
|
|
'terminal or an unknown file extension.')
|
|
operation.add_argument(
|
|
'-O', metavar='OPTION=value[,OPTION=value,...]', action='append',
|
|
help='Give options to the lexer and formatter as a comma-separated '
|
|
'list of key-value pairs. '
|
|
'Example: `-O bg=light,python=cool`.')
|
|
operation.add_argument(
|
|
'-P', metavar='OPTION=value', action='append',
|
|
help='Give a single option to the lexer and formatter - with this '
|
|
'you can pass options whose value contains commas and equal signs. '
|
|
'Example: `-P "heading=Pygments, the Python highlighter"`.')
|
|
operation.add_argument(
|
|
'-o', metavar='OUTPUTFILE',
|
|
help='Where to write the output. Defaults to standard output.')
|
|
|
|
operation.add_argument(
|
|
'INPUTFILE', nargs='?',
|
|
help='Where to read the input. Defaults to standard input.')
|
|
|
|
flags = parser.add_argument_group('Operation flags')
|
|
flags.add_argument(
|
|
'-v', action='store_true',
|
|
help='Print a detailed traceback on unhandled exceptions, which '
|
|
'is useful for debugging and bug reports.')
|
|
flags.add_argument(
|
|
'-s', action='store_true',
|
|
help='Process lines one at a time until EOF, rather than waiting to '
|
|
'process the entire file. This only works for stdin, only for lexers '
|
|
'with no line-spanning constructs, and is intended for streaming '
|
|
'input such as you get from `tail -f`. '
|
|
'Example usage: `tail -f sql.log | pygmentize -s -l sql`.')
|
|
flags.add_argument(
|
|
'-x', action='store_true',
|
|
help='Allow custom lexers and formatters to be loaded from a .py file '
|
|
'relative to the current working directory. For example, '
|
|
'`-l ./customlexer.py -x`. By default, this option expects a file '
|
|
'with a class named CustomLexer or CustomFormatter; you can also '
|
|
'specify your own class name with a colon (`-l ./lexer.py:MyLexer`). '
|
|
'Users should be very careful not to use this option with untrusted '
|
|
'files, because it will import and run them.')
|
|
flags.add_argument('--json', help='Output as JSON. This can '
|
|
'be only used in conjunction with -L.',
|
|
default=False,
|
|
action='store_true')
|
|
|
|
special_modes_group = parser.add_argument_group(
|
|
'Special modes - do not do any highlighting')
|
|
special_modes = special_modes_group.add_mutually_exclusive_group()
|
|
special_modes.add_argument(
|
|
'-S', metavar='STYLE -f formatter',
|
|
help='Print style definitions for STYLE for a formatter '
|
|
'given with -f. The argument given by -a is formatter '
|
|
'dependent.')
|
|
special_modes.add_argument(
|
|
'-L', nargs='*', metavar='WHAT',
|
|
help='List lexers, formatters, styles or filters -- '
|
|
'give additional arguments for the thing(s) you want to list '
|
|
'(e.g. "styles"), or omit them to list everything.')
|
|
special_modes.add_argument(
|
|
'-N', metavar='FILENAME',
|
|
help='Guess and print out a lexer name based solely on the given '
|
|
'filename. Does not take input or highlight anything. If no specific '
|
|
'lexer can be determined, "text" is printed.')
|
|
special_modes.add_argument(
|
|
'-C', action='store_true',
|
|
help='Like -N, but print out a lexer name based solely on '
|
|
'a given content from standard input.')
|
|
special_modes.add_argument(
|
|
'-H', action='store', nargs=2, metavar=('NAME', 'TYPE'),
|
|
help='Print detailed help for the object <name> of type <type>, '
|
|
'where <type> is one of "lexer", "formatter" or "filter".')
|
|
special_modes.add_argument(
|
|
'-V', action='store_true',
|
|
help='Print the package version.')
|
|
special_modes.add_argument(
|
|
'-h', '--help', action='store_true',
|
|
help='Print this help.')
|
|
special_modes_group.add_argument(
|
|
'-a', metavar='ARG',
|
|
help='Formatter-specific additional argument for the -S (print '
|
|
'style sheet) mode.')
|
|
|
|
argns = parser.parse_args(args[1:])
|
|
|
|
try:
|
|
return main_inner(parser, argns)
|
|
except BrokenPipeError:
|
|
# someone closed our stdout, e.g. by quitting a pager.
|
|
return 0
|
|
except Exception:
|
|
if argns.v:
|
|
print(file=sys.stderr)
|
|
print('*' * 65, file=sys.stderr)
|
|
print('An unhandled exception occurred while highlighting.',
|
|
file=sys.stderr)
|
|
print('Please report the whole traceback to the issue tracker at',
|
|
file=sys.stderr)
|
|
print('<https://github.com/pygments/pygments/issues>.',
|
|
file=sys.stderr)
|
|
print('*' * 65, file=sys.stderr)
|
|
print(file=sys.stderr)
|
|
raise
|
|
import traceback
|
|
info = traceback.format_exception(*sys.exc_info())
|
|
msg = info[-1].strip()
|
|
if len(info) >= 3:
|
|
# extract relevant file and position info
|
|
msg += '\n (f{})'.format(info[-2].split('\n')[0].strip()[1:])
|
|
print(file=sys.stderr)
|
|
print('*** Error while highlighting:', file=sys.stderr)
|
|
print(msg, file=sys.stderr)
|
|
print('*** If this is a bug you want to report, please rerun with -v.',
|
|
file=sys.stderr)
|
|
return 1
|