Skip to content

Instantly share code, notes, and snippets.

@luw2007
Last active February 26, 2026 09:03
Show Gist options
  • Select an option

  • Save luw2007/8c4a02471ccae02ddcda6093aff22900 to your computer and use it in GitHub Desktop.

Select an option

Save luw2007/8c4a02471ccae02ddcda6093aff22900 to your computer and use it in GitHub Desktop.
clip2file — 剪贴板转Markdown文件 | Clipboard to Markdown, paste as attachment
#!/usr/bin/env python3
from __future__ import annotations
"""
clip2file.py — 将剪贴板中的长文本/富文本保存为 Markdown 文件,
并将"文件对象"写回剪贴板(可在邮件/聊天/IDE 中直接粘贴附件)。
富文本处理策略(参考 paste-to-markdown Raycast 扩展):
1. 用 osascript 读取剪贴板的 HTML 格式(保留链接、格式信息)
2. 用内置 html.parser 将 HTML 转为 Markdown(不转义下划线等正文字符)
3. 回退:若剪贴板无 HTML,直接使用 pbpaste 纯文本
支持的 HTML 标签:
块级:h1-h6, p, pre, ul, ol, li, blockquote, div, hr, table/tr/th/td
行内:a, strong/b, em/i, code, br, span
用法:
# 模式 A:剪贴板 → Markdown 文件 → 文件对象写回剪贴板
python3 ~/bin/clip2file.py [--threshold 2000] [--outdir /tmp] [-f] [-v]
# 模式 B:指定文件 → 文件对象写入剪贴板(可粘贴为附件)
python3 ~/bin/clip2file.py /path/to/file
"""
import argparse
import base64
import hashlib
import os
import platform
import re
import subprocess
import sys
import textwrap
from datetime import datetime
from typing import List, Optional, Tuple
from html.parser import HTMLParser
# ─── 默认配置 ──────────────────────────────────────────────────────────────────
DEFAULT_THRESHOLD = 2000
DEFAULT_OUTDIR = "/tmp"
# ──────────────────────────────────────────────────────────────────────────────
# ══════════════════════════════════════════════════════════════════════════════
# HTML → Markdown 转换器
# 设计原则:直接保留正文文字,不对 _ * 等字符做转义
# ══════════════════════════════════════════════════════════════════════════════
class HtmlToMarkdown(HTMLParser):
"""
轻量级 HTML → Markdown 转换器(仅依赖标准库)。
支持标签:
块级:h1-h6, p, pre, ul, ol, li, blockquote, div, hr, table/tr/th/td
行内:a, strong/b, em/i, code, br, span
不支持:img(忽略)
"""
def __init__(self):
super().__init__()
self.result: List[str] = []
# 链接状态
self._link_href: Optional[str] = None
self._link_text: List[str] = []
self._in_link = False
# 格式状态
self._in_pre = False
self._in_code = False # inline <code>
self._in_pre_code = False # <pre><code>
# 列表状态
self._list_stack: List[str] = [] # 'ul' | 'ol'
self._list_counters: list[int] = []
# 其他
self._in_blockquote = False
self._heading_level = 0
self._pending_newlines = 0 # 延迟输出的换行数(避免多余空行)
# 忽略的标签内容
self._ignore_depth = 0 # >0 表示在被忽略的标签里
# 表格状态(参考 turndown-plugin-gfm 实现)
self._in_table = False
self._table_rows: List[List[str]] = [] # 二维数组存储表格内容
self._current_row: List[str] = [] # 当前行的单元格
self._current_cell: List[str] = [] # 当前单元格的文本
self._header_row_count = 0 # thead 中的行数
# ── 内部工具 ──────────────────────────────────────────────────────────────
def _flush_newlines(self):
if self._pending_newlines:
self.result.append("\n" * self._pending_newlines)
self._pending_newlines = 0
def _emit(self, text: str):
self._flush_newlines()
self.result.append(text)
def _require_newlines(self, n: int):
self._pending_newlines = max(self._pending_newlines, n)
def _list_indent(self) -> str:
return " " * (len(self._list_stack) - 1)
def _flush_table(self):
"""将收集的表格数据输出为 Markdown 表格。"""
if not self._table_rows:
return
# 计算每列最大宽度(用于对齐,可选)
col_count = max(len(row) for row in self._table_rows) if self._table_rows else 0
if col_count == 0:
return
# 规范化行(补齐列数)
for row in self._table_rows:
while len(row) < col_count:
row.append("")
# 输出表头行
header_idx = min(1, len(self._table_rows)) # 至少第一行作为表头
if self._header_row_count > 0:
header_idx = self._header_row_count
# 输出表格
self._require_newlines(2)
self._flush_newlines()
for i, row in enumerate(self._table_rows):
# 清理单元格内容(去除首尾空白、替换换行为空格)
cells = [cell.strip().replace("\n", " ").replace("\r", "") for cell in row]
line = "| " + " | ".join(cells) + " |"
self.result.append(line)
self.result.append("\n")
# 在表头后插入分隔行
if i == header_idx - 1:
sep = "| " + " | ".join(["---"] * col_count) + " |"
self.result.append(sep)
self.result.append("\n")
self._require_newlines(1)
# 重置表格状态
self._table_rows = []
self._current_row = []
self._current_cell = []
self._header_row_count = 0
# ── 标签处理 ──────────────────────────────────────────────────────────────
def handle_starttag(self, tag: str, attrs):
if self._ignore_depth > 0:
if tag in ("script", "style", "head"):
self._ignore_depth += 1
return
attrs = dict(attrs)
# 忽略 script / style / head 内容
if tag in ("script", "style", "head"):
self._ignore_depth += 1
return
if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
self._heading_level = int(tag[1])
self._require_newlines(2)
self._flush_newlines()
self.result.append("#" * self._heading_level + " ")
elif tag in ("p", "div"):
self._require_newlines(2)
elif tag == "br":
self._emit("\n")
elif tag == "hr":
self._require_newlines(2)
self._flush_newlines()
self.result.append("---")
self._require_newlines(2)
elif tag == "a":
self._in_link = True
self._link_href = attrs.get("href", "")
self._link_text = []
elif tag in ("strong", "b"):
self._emit("**")
elif tag in ("em", "i"):
self._emit("*")
elif tag == "code":
if self._in_pre:
self._in_pre_code = True
else:
self._in_code = True
self._emit("`")
elif tag == "pre":
self._in_pre = True
# 尝试检测语言(class="language-python" 等)
cls = attrs.get("class", "")
lang = ""
m = re.search(r"language-(\w+)", cls)
if m:
lang = m.group(1)
self._require_newlines(2)
self._flush_newlines()
self.result.append(f"```{lang}\n")
elif tag in ("ul", "ol"):
self._list_stack.append(tag)
self._list_counters.append(0)
if len(self._list_stack) == 1:
self._require_newlines(1)
elif tag == "li":
self._flush_newlines()
indent = self._list_indent()
if self._list_stack and self._list_stack[-1] == "ol":
self._list_counters[-1] += 1
self.result.append(f"{indent}{self._list_counters[-1]}. ")
else:
self.result.append(f"{indent}- ")
elif tag == "blockquote":
self._in_blockquote = True
self._require_newlines(2)
self._flush_newlines()
self.result.append("> ")
# 表格标签处理(参考 turndown-plugin-gfm)
elif tag == "table":
self._in_table = True
self._table_rows = []
self._header_row_count = 0
elif tag == "thead":
pass # 标记后续行为表头
elif tag == "tbody":
pass # 不需要特殊处理
elif tag == "tr":
self._current_row = []
elif tag in ("th", "td"):
self._current_cell = []
def handle_endtag(self, tag: str):
if self._ignore_depth > 0:
if tag in ("script", "style", "head"):
self._ignore_depth -= 1
return
if tag in ("h1", "h2", "h3", "h4", "h5", "h6"):
self._heading_level = 0
self._require_newlines(2)
elif tag in ("p", "div"):
self._require_newlines(2)
elif tag == "a":
text = "".join(self._link_text).strip()
href = (self._link_href or "").strip()
# 如果 href 与文字相同,或者 href 为空,直接输出文字
if href and href != text and not href.startswith("javascript:"):
self._emit(f"[{text}]({href})")
elif text:
self._emit(text)
self._in_link = False
self._link_href = None
self._link_text = []
elif tag in ("strong", "b"):
self._emit("**")
elif tag in ("em", "i"):
self._emit("*")
elif tag == "code":
if self._in_pre_code:
self._in_pre_code = False
else:
self._in_code = False
self._emit("`")
elif tag == "pre":
self._in_pre = False
self.result.append("\n```")
self._require_newlines(2)
elif tag in ("ul", "ol"):
if self._list_stack:
self._list_stack.pop()
self._list_counters.pop()
if not self._list_stack:
self._require_newlines(1)
elif tag == "li":
self._require_newlines(1)
elif tag == "blockquote":
self._in_blockquote = False
self._require_newlines(2)
# 表格结束标签处理
elif tag == "table":
self._flush_table()
self._in_table = False
elif tag == "thead":
# thead 结束时,记录表头行数
self._header_row_count = len(self._table_rows)
elif tag == "tbody":
pass
elif tag == "tr":
# 行结束:将当前行加入表格
if self._current_row or self._in_table:
self._table_rows.append(self._current_row)
self._current_row = []
elif tag in ("th", "td"):
# 单元格结束:合并文本并加入当前行
cell_text = "".join(self._current_cell)
self._current_row.append(cell_text)
self._current_cell = []
def handle_data(self, data: str):
if self._ignore_depth > 0:
return
if self._in_pre:
# pre 块内直接原样输出,不做任何处理
self._flush_newlines()
self.result.append(data)
return
# 表格模式:文本写入当前单元格
if self._in_table:
self._current_cell.append(data)
return
# 正文文字:直接保留,不转义任何字符(_ * 等保持原样)
if self._in_link:
self._link_text.append(data)
else:
self._flush_newlines()
self.result.append(data)
def handle_entityref(self, name: str):
"""处理 HTML 实体,如 &amp; &nbsp;"""
entities = {
"amp": "&", "lt": "<", "gt": ">", "quot": '"',
"apos": "'", "nbsp": " ", "mdash": "—", "ndash": "–",
"ldquo": "\u201c", "rdquo": "\u201d",
"lsquo": "\u2018", "rsquo": "\u2019",
"hellip": "…", "copy": "©", "reg": "®", "trade": "™",
}
char = entities.get(name, f"&{name};")
if self._in_link:
self._link_text.append(char)
else:
self._flush_newlines()
self.result.append(char)
def handle_charref(self, name: str):
"""处理数字字符引用,如 &#160; &#x00A0;"""
try:
if name.startswith("x") or name.startswith("X"):
char = chr(int(name[1:], 16))
else:
char = chr(int(name))
except (ValueError, OverflowError):
char = ""
if self._in_link:
self._link_text.append(char)
else:
self._flush_newlines()
self.result.append(char)
def get_markdown(self) -> str:
md = "".join(self.result).strip()
# 压缩连续超过 2 个空行为 2 个
md = re.sub(r"\n{3,}", "\n\n", md)
return md
def html_to_markdown(html: str) -> str:
"""将 HTML 字符串转换为 Markdown 字符串。"""
parser = HtmlToMarkdown()
parser.feed(html)
return parser.get_markdown()
# ══════════════════════════════════════════════════════════════════════════════
# 剪贴板读取(优先读 HTML,回退到纯文本)
# ══════════════════════════════════════════════════════════════════════════════
def _extract_html_charset(data: bytes) -> Optional[str]:
head = data[:4096]
try:
head_text = head.decode("ascii", errors="ignore").lower()
except Exception:
return None
m = re.search(r"charset\s*=\s*['\"]?\s*([a-z0-9._-]+)", head_text)
if not m:
return None
charset = m.group(1).strip()
return charset or None
def _decode_bytes_with_fallback(data: bytes, encodings: List[str]) -> str:
last_error: Optional[Exception] = None
for enc in encodings:
try:
return data.decode(enc)
except Exception as e:
last_error = e
continue
if last_error:
raise last_error
raise UnicodeDecodeError("utf-8", b"", 0, 0, "empty input")
def _decode_clipboard_text_bytes(data: bytes) -> str:
if data.startswith(b"\xef\xbb\xbf"):
return data.decode("utf-8-sig")
if data.startswith(b"\xff\xfe") or data.startswith(b"\xfe\xff"):
return data.decode("utf-16")
if b"\x00" in data and len(data) >= 4:
try:
return data.decode("utf-16")
except Exception:
pass
return _decode_bytes_with_fallback(data, ["utf-8", "utf-8-sig", "utf-16", "gb18030"])
def _decode_clipboard_html_bytes(data: bytes) -> str:
charset = _extract_html_charset(data)
if charset:
try:
return data.decode(charset)
except Exception:
pass
return _decode_clipboard_text_bytes(data)
def get_clipboard_html_macos_appkit() -> str:
try:
import AppKit # type: ignore
except Exception:
return ""
pb = AppKit.NSPasteboard.generalPasteboard()
try_types = []
for name in ["NSPasteboardTypeHTML", "NSHTMLPboardType"]:
t = getattr(AppKit, name, None)
if isinstance(t, str):
try_types.append(t)
try_types.extend(["public.html", "text/html"])
for t in try_types:
try:
data = pb.dataForType_(t)
except Exception:
data = None
if not data:
continue
try:
b64 = data.base64EncodedStringWithOptions_(0)
raw = base64.b64decode(str(b64))
html = _decode_clipboard_html_bytes(raw).strip()
if html:
return html
except Exception:
continue
return ""
def get_clipboard_plain_macos_appkit() -> str:
try:
import AppKit # type: ignore
except Exception:
return ""
pb = AppKit.NSPasteboard.generalPasteboard()
try_types = []
for name in ["NSPasteboardTypeString", "NSStringPboardType"]:
t = getattr(AppKit, name, None)
if isinstance(t, str):
try_types.append(t)
try_types.extend(["public.utf8-plain-text", "public.plain-text", "text/plain"])
for t in try_types:
try:
s = pb.stringForType_(t)
except Exception:
s = None
if s:
return str(s)
return ""
def get_clipboard_html_macos() -> str:
"""
macOS:通过 osascript 读取剪贴板的 HTML 格式。
命令来自 paste-to-markdown Raycast 扩展。
返回空字符串表示剪贴板无 HTML 数据。
"""
try:
result = subprocess.run(
[
"osascript", "-e",
"the clipboard as «class HTML»"
" | perl -ne 'print chr foreach unpack(\"C*\",pack(\"H*\",substr($_,11,-3)))'"
],
capture_output=True, timeout=3
)
# osascript 返回十六进制编码的 HTML,perl 负责解码
if result.returncode == 0:
return _decode_clipboard_html_bytes(result.stdout).strip()
except Exception:
pass
return ""
def get_clipboard_html_macos_v2() -> str:
"""
macOS 备用方案:用 shell=True 执行管道命令(与 Raycast 扩展完全一致)。
"""
try:
result = subprocess.run(
"osascript -e 'the clipboard as «class HTML»'"
" | perl -ne 'print chr foreach unpack(\"C*\",pack(\"H*\",substr($_,11,-3)))'",
shell=True, capture_output=True, timeout=3
)
if result.returncode == 0:
html = _decode_clipboard_html_bytes(result.stdout).strip()
if html.startswith("<"):
return html
except Exception:
pass
return ""
def get_clipboard_plain_macos() -> str:
result = subprocess.run(["pbpaste"], capture_output=True)
return _decode_clipboard_text_bytes(result.stdout)
def get_clipboard_linux() -> str:
for cmd in [["xclip", "-selection", "clipboard", "-o"],
["xsel", "--clipboard", "--output"],
["wl-paste"]]:
try:
result = subprocess.run(cmd, capture_output=True)
if result.returncode == 0:
return _decode_clipboard_text_bytes(result.stdout)
except FileNotFoundError:
continue
raise RuntimeError("未找到可用的剪贴板工具 (xclip / xsel / wl-paste)。")
def get_clipboard_windows() -> str:
result = subprocess.run(
["powershell", "-Command", "Get-Clipboard"],
capture_output=True
)
return _decode_clipboard_text_bytes(result.stdout)
def get_clipboard_as_markdown(verbose: bool = False) -> Tuple[str, str]:
"""
读取剪贴板,返回 (markdown_text, source_type)。
source_type: 'html' | 'plain'
"""
system = platform.system()
if system == "Darwin":
# 1. 优先读 HTML(保留链接、格式)
html = get_clipboard_html_macos_appkit()
if not html:
html = get_clipboard_html_macos_v2()
if not html:
html = get_clipboard_html_macos()
if html and html.strip().startswith("<"):
if verbose:
print(f"[INFO] 剪贴板含 HTML ({len(html)} chars),转换为 Markdown")
md = html_to_markdown(html)
if md.strip():
return md, "html"
# 2. 回退:纯文本
if verbose:
print("[INFO] 剪贴板无 HTML,使用纯文本")
plain = get_clipboard_plain_macos_appkit()
if plain:
return plain, "plain"
return get_clipboard_plain_macos(), "plain"
elif system == "Linux":
return get_clipboard_linux(), "plain"
elif system == "Windows":
return get_clipboard_windows(), "plain"
else:
raise RuntimeError(f"不支持的操作系统: {system}")
# ══════════════════════════════════════════════════════════════════════════════
# 文件对象写入剪贴板
# ══════════════════════════════════════════════════════════════════════════════
def _run_applescript_with_args(script: str, args: list) -> subprocess.CompletedProcess:
"""以 argv 模式执行 AppleScript,自动附加参数。"""
return subprocess.run(["osascript", "-e", script, "--", *args], capture_output=True)
def set_clipboard_file_macos(filepath: str) -> None:
"""
macOS:将文件对象写入 NSPasteboard(三层回退策略)。
策略优先级:
1. pyobjc (AppKit) — 直接操作 NSPasteboard,最准确,无副作用
2. osascript alias — 先转 alias 再写入,不依赖 Finder
3. osascript Finder — 通过 Finder 写入,兼容性最好但会激活 Finder
注意:'set the clipboard to POSIX file "..."'(不转 alias)
写入的是字符串,不是文件对象,已废弃。
"""
abs_path = os.path.abspath(filepath)
# ── 策略 1:pyobjc(macOS 系统自带,无需安装)────────────────────────
try:
from AppKit import NSPasteboard, NSURL # type: ignore
pb = NSPasteboard.generalPasteboard()
pb.clearContents()
url = NSURL.fileURLWithPath_(abs_path)
ok = pb.writeObjects_([url])
if ok:
return
except Exception:
pass # pyobjc 不可用时回退
# ── 策略 2:osascript alias(推荐回退)───────────────────────────────
alias_script = textwrap.dedent("""
on run argv
set p to item 1 of argv
set f to POSIX file p as alias
set the clipboard to f
end run
""").strip()
result = _run_applescript_with_args(alias_script, [abs_path])
if result.returncode == 0:
return
# ── 策略 3:osascript via Finder(最后手段)──────────────────────────
finder_script = textwrap.dedent("""
on run argv
set p to item 1 of argv
tell application "Finder" to set the clipboard to POSIX file p
end run
""").strip()
result = _run_applescript_with_args(finder_script, [abs_path])
if result.returncode == 0:
return
err = result.stderr.decode("utf-8", errors="replace").strip()
raise RuntimeError(f"所有剪贴板写入策略均失败: {err}")
def set_clipboard_file_linux(filepath: str) -> None:
"""Linux:用 xclip 写入 text/uri-list(FreeDesktop 标准)。"""
uri = f"file://{os.path.abspath(filepath)}\r\n"
for cmd in [["xclip", "-selection", "clipboard", "-t", "text/uri-list"],
["wl-copy", "--type", "text/uri-list"]]:
try:
result = subprocess.run(cmd, input=uri.encode("utf-8"), capture_output=True)
if result.returncode == 0:
return
except FileNotFoundError:
continue
# 降级到路径字符串
for cmd in [["xclip", "-selection", "clipboard"],
["xsel", "--clipboard", "--input"],
["wl-copy"]]:
try:
subprocess.run(cmd, input=filepath.encode("utf-8"), check=True)
return
except FileNotFoundError:
continue
raise RuntimeError("未找到可用的剪贴板工具。")
def set_clipboard_file_windows(filepath: str) -> None:
"""Windows:PowerShell SetFileDropList。"""
abs_path = os.path.abspath(filepath).replace("\\", "\\\\")
ps_script = textwrap.dedent(f"""
Add-Type -AssemblyName System.Windows.Forms
$files = New-Object System.Collections.Specialized.StringCollection
$files.Add("{abs_path}")
[System.Windows.Forms.Clipboard]::SetFileDropList($files)
""").strip()
result = subprocess.run(
["powershell", "-NoProfile", "-Command", ps_script],
capture_output=True
)
if result.returncode != 0:
err = result.stderr.decode("utf-8", errors="replace").strip()
raise RuntimeError(f"PowerShell 失败: {err}")
def set_clipboard_file(filepath: str) -> None:
system = platform.system()
if system == "Darwin":
set_clipboard_file_macos(filepath)
elif system == "Linux":
set_clipboard_file_linux(filepath)
elif system == "Windows":
set_clipboard_file_windows(filepath)
else:
raise RuntimeError(f"不支持的操作系统: {system}")
# ══════════════════════════════════════════════════════════════════════════════
# 辅助函数
# ══════════════════════════════════════════════════════════════════════════════
def build_markdown_file(text: str, source_type: str) -> str:
"""为 Markdown 文件添加元信息注释头。"""
now = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
header = textwrap.dedent(f"""\
<!-- clip2file: auto-generated -->
<!-- created: {now} -->
<!-- source: {source_type} -->
<!-- chars: {len(text)} -->
""")
# 若已有 Markdown 标题则直接拼接;否则补充默认标题
if text.lstrip().startswith("#"):
return header + text
return header + "# Clipboard Content\n\n" + text
def make_filename(text: str, outdir: str) -> str:
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
digest = hashlib.md5(text.encode()).hexdigest()[:6]
return os.path.join(outdir, f"clip_{ts}_{digest}.md")
def notify(message: str) -> None:
if platform.system() == "Darwin":
notify_script = textwrap.dedent("""
on run argv
display notification (item 1 of argv) with title "clip2file"
end run
""").strip()
try:
_run_applescript_with_args(notify_script, [message])
except Exception:
pass
# ══════════════════════════════════════════════════════════════════════════════
# 主入口
# ══════════════════════════════════════════════════════════════════════════════
def main() -> None:
parser = argparse.ArgumentParser(
description=(
"将剪贴板长文本/富文本保存为 Markdown,"
"并将文件对象写回剪贴板(可粘贴为附件)。\n"
"也可以直接传入文件路径,将该文件对象写入剪贴板。"
)
)
parser.add_argument("file", nargs="?", default=None,
help="直接指定文件路径,将其作为文件对象写入剪贴板(跳过剪贴板读取流程)")
parser.add_argument("--threshold", "-t", type=int, default=DEFAULT_THRESHOLD,
help=f"触发保存的最小字符数 (默认: {DEFAULT_THRESHOLD})")
parser.add_argument("--outdir", "-o", default=DEFAULT_OUTDIR,
help=f"输出目录 (默认: {DEFAULT_OUTDIR})")
parser.add_argument("--force", "-f", action="store_true",
help="无论长度是否达到阈值,强制保存")
parser.add_argument("--verbose", "-v", action="store_true",
help="打印详细日志")
args = parser.parse_args()
# ── 模式 A:直接传入文件路径 → 将文件对象写入剪贴板 ──────────────────
if args.file is not None:
filepath = os.path.expanduser(args.file)
filepath = os.path.abspath(filepath)
if not os.path.exists(filepath):
print(f"[ERROR] 文件不存在: {filepath}", file=sys.stderr)
sys.exit(1)
if not os.path.isfile(filepath):
print(f"[ERROR] 路径不是文件: {filepath}", file=sys.stderr)
sys.exit(1)
if args.verbose:
print(f"[INFO] 直接模式:将文件写入剪贴板 → {filepath}")
try:
set_clipboard_file(filepath)
except Exception as e:
print(f"[ERROR] 无法将文件对象写入剪贴板: {e}", file=sys.stderr)
sys.exit(1)
msg = f"已将文件对象写入剪贴板 → {filepath}"
print(f"[OK] {msg}")
notify(msg)
return
# ── 模式 B:原有流程(读取剪贴板 → 保存为 Markdown → 写回文件对象)───
# 1. 读取剪贴板(HTML 优先,自动转 Markdown)
try:
text, source_type = get_clipboard_as_markdown(verbose=args.verbose)
except Exception as e:
print(f"[ERROR] 无法读取剪贴板: {e}", file=sys.stderr)
sys.exit(1)
char_count = len(text)
if args.verbose:
print(f"[INFO] 字符数: {char_count},来源: {source_type},阈值: {args.threshold}")
# 2. 阈值判断
if not args.force and char_count <= args.threshold:
msg = f"[SKIP] 文本长度 {char_count} ≤ 阈值 {args.threshold},无需转存。"
print(msg)
sys.exit(0)
# 3. 写文件
os.makedirs(args.outdir, exist_ok=True)
filepath = make_filename(text, args.outdir)
md_content = build_markdown_file(text, source_type)
try:
with open(filepath, "w", encoding="utf-8") as f:
f.write(md_content)
except OSError as e:
print(f"[ERROR] 无法写入文件: {e}", file=sys.stderr)
sys.exit(1)
if args.verbose:
print(f"[INFO] 已写入: {filepath} ({os.path.getsize(filepath)} bytes)")
# 4. 将文件对象写入剪贴板
try:
set_clipboard_file(filepath)
except Exception as e:
print(f"[ERROR] 无法将文件对象写入剪贴板: {e}", file=sys.stderr)
sys.exit(1)
# 5. 反馈
msg = f"已保存 {char_count} 字符({source_type}) → {filepath}"
print(f"[OK] {msg}")
notify(msg)
if __name__ == "__main__":
main()
#!/bin/bash
# Required parameters:
# @raycast.schemaVersion 1
# @raycast.title Clip to File
# @raycast.mode silent
# Optional parameters:
# @raycast.icon 📋
# @raycast.packageName Clipboard Utils
# @raycast.shortcut cmd+shift+f
python3 ~/bin/clip2file.py
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment