Skip to content

Instantly share code, notes, and snippets.

@frederik-elwert
Created November 5, 2024 16:53
Show Gist options
  • Save frederik-elwert/a8db9d9b2de4b33db6f32f4c8495a0c9 to your computer and use it in GitHub Desktop.
Save frederik-elwert/a8db9d9b2de4b33db6f32f4c8495a0c9 to your computer and use it in GitHub Desktop.
Nautilus extension to allow for converting Markdown files via pandoc. Copy to ~/.local/share/nautilus-python/extensions/
#!/usr/bin/env python3
import os
import re
import subprocess
from multiprocessing import Process
from pathlib import Path
from typing import Dict, List, Optional
from urllib.parse import unquote
import yaml
from gi.repository import GObject, Nautilus
class PandocConverterExtension(GObject.GObject, Nautilus.MenuProvider):
FORMAT_EXTENSIONS = {
"docx": ".docx",
"epub": ".epub",
"html": ".html",
"markdown": ".md",
"odt": ".odt",
"pdf": ".pdf",
"pptx": ".pptx",
"tei": ".xml",
"latex": ".tex",
"typst": ".typ",
"revealjs": ".html",
}
def __init__(self):
super().__init__()
self.defaults_files = self._find_defaults_files()
print("Initializing Nautilus Pandoc Converter")
def _get_pandoc_data_dir(self) -> Optional[Path]:
"""Find the Pandoc data directory."""
xdg_data_home = Path(os.getenv("XDG_DATA_HOME", "~/.local/share")).expanduser()
possible_paths = [xdg_data_home / "pandoc", Path.home() / ".pandoc"]
for path in possible_paths:
if path.is_dir():
return path
return None
def _find_defaults_files(self) -> Dict[str, Path]:
"""Find all defaults files in the Pandoc data directory."""
defaults_files = {}
data_dir = self._get_pandoc_data_dir()
if not data_dir:
return defaults_files
defaults_dir = data_dir / "defaults"
if not defaults_dir.is_dir():
return defaults_files
for filepath in defaults_dir.glob("*.yaml"):
try:
config = yaml.safe_load(filepath.read_text())
# Check if the config specifies an output format
output_format = config.get("to") or config.get("write")
if output_format:
menu_name = filepath.stem
defaults_files[menu_name] = filepath
except (yaml.YAMLError, IOError):
continue
return defaults_files
def _get_output_path(self, input_path: Path, output_format: str) -> Path:
"""Generate the output path based on the input path and output format."""
base_format = re.split(r"[+-]", output_format)[0]
extension = self.FORMAT_EXTENSIONS.get(base_format, f".{base_format}")
return input_path.with_name(f"{input_path.stem}_converted{extension}")
@staticmethod
def _run_conversion(input_path: str, defaults_file: str) -> None:
"""Static method to run the conversion in a separate process."""
try:
# Read the output format from the defaults file
with open(defaults_file, "r") as f:
config = yaml.safe_load(f)
output_format = config.get("to") or config.get("write")
if not output_format:
return
# Strip extensions and get output path
base_format = re.split(r"[+-]", output_format)[0]
extension = PandocConverterExtension.FORMAT_EXTENSIONS.get(
base_format, f".{base_format}"
)
input_path_obj = Path(input_path)
output_path = input_path_obj.with_name(
f"{input_path_obj.stem}_converted{extension}"
)
# Run pandoc
subprocess.run(
["pandoc", "-d", defaults_file, input_path, "-o", str(output_path)],
check=True,
)
# Send notification on completion
subprocess.run(
[
"notify-send",
"Pandoc Conversion Complete",
f"Converted {input_path_obj.name} to {output_path.name}",
]
)
except Exception as e:
# Send notification on error
subprocess.run(
[
"notify-send",
"Pandoc Conversion Error",
f"Error converting {input_path_obj.name}: {str(e)}",
]
)
def _convert_file(self, input_path: Path, defaults_file: Path) -> None:
"""Start the conversion process in a separate process."""
# Create and start the conversion process
Process(
target=self._run_conversion,
args=(str(input_path), str(defaults_file)),
daemon=True,
).start()
def get_file_items(self, files: List[Nautilus.FileInfo]) -> List[Nautilus.MenuItem]:
"""Create menu items for Markdown files."""
if len(files) != 1:
return []
file_info = files[0]
file_path = Path(unquote(file_info.get_uri()[7:])) # Remove 'file://' prefix
# Check if this is a Markdown file
if file_path.suffix.lower() not in [".md", ".markdown"]:
return []
# Create the main menu item
convert_item = Nautilus.MenuItem(
name="PandocConverter::Convert", label="Convert", tip="Convert using Pandoc"
)
# Create the submenu
submenu = Nautilus.Menu()
convert_item.set_submenu(submenu)
# Add submenu items for each defaults file
for menu_name, defaults_path in self.defaults_files.items():
sub_item = Nautilus.MenuItem(
name=f"PandocConverter::Convert::{menu_name}",
label=menu_name,
tip=f"Convert using {menu_name} defaults",
)
sub_item.connect(
"activate",
lambda w, path=file_path, df=defaults_path: self._convert_file(
path, df
),
)
submenu.append_item(sub_item)
return [convert_item] if self.defaults_files else []
def get_background_items(self, current_folder) -> List[Nautilus.MenuItem]:
"""Required by the interface, but we don't need background items."""
return []
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment