马儿慢些走

人生惶惶,悲喜一场。

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)

用户需手动添加自定义快捷键:

  1. 打开“设置” → “键盘” → “查看和自定义快捷键”。
  2. 滚动至底部,点击“+”添加:
    • 名称: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()

posted on 2026-03-22 08:00  马儿慢些走  阅读(61)  评论(0)    收藏  举报

导航