Linux 系统下 Flomo 浮墨笔记快捷输入的替代方案
本文由 DeepSeek AI 大模型生成,仅供参考。
2026年03月21日
Flomo Quick Note 开发文档
1. 项目概述
Flomo Quick Note 是一个为 Linux 桌面环境(特别是 GNOME)设计的轻量级灵感记录工具。它利用
Flomo 的 Webhook API,通过简洁的图形化输入界面,帮助用户快速记录想法,并支持多行输入、每日
发送限制、配置持久化等功能。项目以 Python 3 为核心,借助系统原生工具(Zenity、notify-send)
实现最小依赖和高集成度。
1.1 主要特性
- 全局唤起:可通过 GNOME 自定义快捷键或桌面图标快速启动。
- 多行输入:基于 Zenity 的文本编辑对话框,支持长文本和换行。
- 每日限额保护:自动跟踪当日已发送数量,达到 Flomo 免费版每日 100 条上限时给出提示并阻止发送。
- 配置向导:首次运行时自动引导用户输入 Webhook 地址,配置保存在 XDG 规范目录。
- 即时反馈:发送成功或失败后,通过系统通知提醒用户。
- 离线友好:无网络时自动处理异常并提示。
2. 系统要求
- 操作系统:任何支持 Python 3.6+ 的 Linux 发行版(推荐 Fedora/GNOME)。
- 必需软件包:
python3(通常预装)zenity(用于图形对话框)notify-send(一般随桌面环境自带,若缺失可安装libnotify)
- 可选增强:
python3-requests(若安装,脚本将优先使用,但未安装时回退到urllib)
3. 安装与部署
3.1 安装依赖
sudo dnf install zenity # Fedora
# 或其它包管理器:apt install zenity 等
3.2 获取脚本
将以下文件放置到用户目录下的适当位置:
- 主脚本:
~/.local/bin/flomo-quick - 配置文件目录:
~/.config/flomo-quick/(脚本首次运行自动创建) - 桌面入口文件(可选):
~/.local/share/applications/flomo-quick.desktop
脚本需赋予执行权限:
chmod +x ~/.local/bin/flomo-quick
3.3 创建桌面快捷方式(可选)
[Desktop Entry]
Version=1.0
Name=Flomo Quick Note
Comment=Quickly capture ideas to Flomo
Exec=/home/用户名/.local/bin/flomo-quick
Icon=flomo-quick
Terminal=false
Type=Application
Categories=Utility;
StartupNotify=true
将图标文件(如 flomo-quick.svg)放入 ~/.local/share/icons/hicolor/scalable/apps/ 后,执行:
update-desktop-database ~/.local/share/applications/
3.4 配置全局快捷键(GNOME)
用户需手动添加自定义快捷键:
- 打开“设置” → “键盘” → “查看和自定义快捷键”。
- 滚动至底部,点击“+”添加:
- 名称:Flomo Quick Note
- 命令:
/usr/bin/python3 /home/用户名/.local/bin/flomo-quick - 快捷键:如
Super+N
4. 使用指南
4.1 首次运行
- 执行脚本(通过快捷键或终端)将弹出配置向导,要求输入 Flomo Webhook 地址。
- 地址格式应为
https://flomoapp.com/api/v1/memo/xxxx-xxxx-xxxx。 - 输入后保存,之后可直接使用。
4.2 正常使用
- 唤起脚本后,出现一个可编辑的多行文本窗口。
- 输入内容,支持任何文本,Flomo 会自动解析标签(如
#工作)。 - 点击“确定”发送,若成功则收到系统通知;失败则显示错误信息。
4.3 每日限额
- 脚本会在本地记录当日发送数量,达到 100 条时弹出警告对话框,并阻止本次发送。
- 每日计数按自然日重置。
5. 代码结构
flomo-quick.py
├── 配置常量定义
├── 工具函数
│ ├── check_zenity() # 检查zenity是否可用
│ ├── notify() # 发送桌面通知
│ ├── load_config() # 读取JSON配置
│ ├── save_config() # 写入JSON配置
│ ├── config_wizard() # 首次配置向导
│ ├── get_user_input() # 获取用户输入(多行)
│ ├── load_counter() # 读取/重置计数器
│ ├── save_counter() # 保存计数器
│ ├── check_daily_limit() # 检查每日限额
│ ├── increment_counter() # 增加计数
│ └── send_to_flomo() # 发送API请求
└── main() # 主流程
6. 关键实现细节
6.1 多行输入
使用 zenity --text-info --editable 创建一个可编辑的文本区域,支持多行输入。返回的文本保留内部换行,但去除末尾多余的换行符。
result = subprocess.run(
[ZENITY_CMD, "--text-info", "--editable",
"--title=Flomo 灵感\n流水不争先,争的是滔滔不绝。",
"--width=600", "--height=400"],
capture_output=True, text=True, check=True
)
text = result.stdout.rstrip('\n')
6.2 每日限额本地持久化
计数器文件 counter.json 存储格式:
{
"date": "2025-03-21",
"count": 42
}
每次发送成功后,count 加 1。若日期变更,自动重置为 0。
6.3 API 发送
使用 urllib.request 发送 POST 请求,超时设为 10 秒。若检测到 requests 库已安装,可替换为更优雅的实现(当前版本未实现回退,但可扩展)。
data = json.dumps({"content": content}).encode('utf-8')
headers = {'Content-Type': 'application/json'}
req = Request(api_url, data=data, headers=headers, method='POST')
with urlopen(req, timeout=10) as response:
# 处理响应
6.4 错误处理
- 配置缺失 → 自动进入向导。
- 配置损坏 → 删除文件并重新配置。
- Zenity 未安装 → 打印错误并退出。
- 网络异常 → 通知用户。
- 用户取消输入 → 无任何副作用。
7. 配置文件说明
7.1 配置文件位置
~/.config/flomo-quick/config.json
7.2 字段定义
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
api_url |
string | 是 | Flomo Webhook 完整地址 |
default_tags |
array | 否 | 预留字段,未来可扩展自动附加标签 |
7.3 示例
{
"api_url": "https://flomoapp.com/api/v1/memo/abc-123-def",
"default_tags": ["灵感"]
}
8. 错误处理与用户提示
| 场景 | 行为 |
|---|---|
| Zenity 未安装 | 终端输出错误信息,退出(无法弹出 GUI) |
| 配置文件不存在 | 自动启动配置向导 |
| 配置文件损坏 | 提示后删除文件,启动向导 |
| 用户取消配置 | 退出,无通知 |
| 输入为空 | 直接退出 |
| 网络错误 | notify-send 红色警告“网络错误: xxx” |
| HTTP 4xx/5xx | 通知显示具体状态码 |
| 每日超限 | Zenity 警告窗口,退出 |
9. 扩展性与未来计划
9.1 可扩展方向
- 选中文本捕获:利用
xclip获取当前选中的文本作为默认输入内容。 - 默认标签附加:从配置中读取
default_tags,自动拼接到用户输入末尾。 - 本地历史记录:保存最近几条灵感,防止意外丢失。
- 多账户支持:通过环境变量或配置文件切换不同 Webhook。
- 增强 API 库:检测
requests并优先使用,提供更好的连接池和错误信息。
9.2 实现建议
若添加上述功能,建议保持模块化,遵循单一职责原则。例如将“输入获取”与“文本预处理”分离,
便于组合。
10. 贡献指南
欢迎提交 Issue 和 Pull Request。开发时请遵循以下规范:
- 代码风格遵循 PEP 8。
- 新增功能需更新文档。
- 保持依赖最小化,优先使用标准库。
- 错误处理要全面,确保用户体验友好。
11. 许可证
本项目采用 MIT 许可证,详见 LICENSE 文件。
最后更新:2026 年 3 月
附录:代码
#!/usr/bin/env python3
"""
Flomo Quick Note (Enhanced)
- Multi-line input via Zenity text-info
- Daily send limit (100) with local counter
"""
import os
import json
import subprocess
import sys
from datetime import date
from urllib.request import Request, urlopen
from urllib.error import URLError, HTTPError
# ---------- Configuration ----------
CONFIG_DIR = os.path.expanduser("~/.config/flomo-quick")
CONFIG_FILE = os.path.join(CONFIG_DIR, "config.json")
COUNTER_FILE = os.path.join(CONFIG_DIR, "counter.json")
ZENITY_CMD = "zenity"
NOTIFY_CMD = "notify-send"
DAILY_LIMIT = 100
# -----------------------------------
def check_zenity():
"""Check if zenity is installed."""
try:
subprocess.run([ZENITY_CMD, "--version"], capture_output=True, check=True)
except (subprocess.CalledProcessError, FileNotFoundError):
print("Zenity not found. Please install it: sudo dnf install zenity", file=sys.stderr)
sys.exit(1)
def notify(message, is_error=False):
"""Show desktop notification."""
urgency = "critical" if is_error else "normal"
subprocess.run([NOTIFY_CMD, "-a", "Flomo", "-u", urgency, message])
def load_config():
"""Load configuration from JSON file. Return None if not exists."""
if not os.path.exists(CONFIG_FILE):
return None
try:
with open(CONFIG_FILE, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, IOError):
return None
def save_config(config):
"""Save configuration to JSON file."""
os.makedirs(CONFIG_DIR, exist_ok=True)
with open(CONFIG_FILE, 'w') as f:
json.dump(config, f, indent=2)
def config_wizard():
"""First-run wizard: ask for Flomo Webhook URL via zenity."""
try:
result = subprocess.run(
[ZENITY_CMD, "--entry", "--title=Flomo 首次设置",
"--text=请输入你的 Flomo Webhook 地址(可在 Flomo 设置中获取):"],
capture_output=True, text=True, check=True
)
url = result.stdout.strip()
if not url:
notify("未输入 Webhook 地址,配置取消", is_error=True)
sys.exit(0)
if not url.startswith("https://"):
subprocess.run([ZENITY_CMD, "--error", "--text=地址必须以 https:// 开头"])
return config_wizard() # retry
config = {"api_url": url}
save_config(config)
notify("Flomo 配置已保存")
return config
except subprocess.CalledProcessError:
# User cancelled
sys.exit(0)
def get_user_input():
"""
Show multi-line input dialog using zenity --text-info.
Returns the entered text, or None if cancelled.
"""
try:
# Use --text-info with editable flag for multi-line input
result = subprocess.run(
[ZENITY_CMD, "--text-info", "--editable",
"--title=Flomo 灵感\n流水不争先,争的是滔滔不绝。", "--width=600", "--height=400"],
capture_output=True, text=True, check=True
)
# Zenity outputs the text exactly as entered, including newlines
text = result.stdout
# Remove trailing newline(s) if the user didn't intend them? We'll strip only at the end.
# But preserve internal newlines. We'll strip trailing whitespace generally.
text = text.rstrip('\n') # remove only trailing newlines, keep internal ones
return text if text else None
except subprocess.CalledProcessError:
# User cancelled (e.g., closed window or pressed Cancel)
return None
def load_counter():
"""Load daily counter, reset if date changed."""
today = date.today().isoformat()
if not os.path.exists(COUNTER_FILE):
return {"date": today, "count": 0}
try:
with open(COUNTER_FILE, 'r') as f:
counter = json.load(f)
if counter.get("date") != today:
counter = {"date": today, "count": 0}
return counter
except (json.JSONDecodeError, IOError):
return {"date": today, "count": 0}
def save_counter(counter):
"""Save daily counter."""
with open(COUNTER_FILE, 'w') as f:
json.dump(counter, f, indent=2)
def check_daily_limit():
"""Check if today's send count has reached limit. If so, show warning and return False."""
counter = load_counter()
if counter["count"] >= DAILY_LIMIT:
# Show warning dialog
subprocess.run([
ZENITY_CMD, "--warning",
"--title=今日发送已达上限",
f"--text=你今天已经记录了 {DAILY_LIMIT} 条灵感,已达 Flomo 每日上限。\n明日再继续记录吧~"
])
return False
return True
def increment_counter():
"""Increment daily counter after successful send."""
counter = load_counter()
counter["count"] += 1
save_counter(counter)
def send_to_flomo(api_url, content):
"""Send content to Flomo via webhook."""
data = json.dumps({"content": content}).encode('utf-8')
headers = {'Content-Type': 'application/json'}
req = Request(api_url, data=data, headers=headers, method='POST')
try:
with urlopen(req, timeout=10) as response:
if response.status == 200:
return True, "灵感已记录"
else:
return False, f"HTTP {response.status}: {response.reason}"
except HTTPError as e:
return False, f"HTTP {e.code}: {e.reason}"
except URLError as e:
return False, f"网络错误: {e.reason}"
except Exception as e:
return False, f"未知错误: {str(e)}"
def main():
check_zenity()
# Load config or run wizard
config = load_config()
if config is None:
config = config_wizard()
if config is None:
sys.exit(0)
api_url = config.get("api_url")
if not api_url:
notify("配置文件中缺少 api_url,请重新配置", is_error=True)
os.remove(CONFIG_FILE)
config = config_wizard()
if not config:
sys.exit(0)
api_url = config["api_url"]
# Check daily limit
if not check_daily_limit():
sys.exit(0)
# Get user input (multi-line)
content = get_user_input()
if content is None:
sys.exit(0) # cancelled or empty
# Send to Flomo
success, message = send_to_flomo(api_url, content)
if success:
increment_counter()
notify(message, is_error=not success)
if __name__ == "__main__":
main()
浙公网安备 33010602011771号