From 5f5a4923424bb51ff57fc5440438c49154abb52b Mon Sep 17 00:00:00 2001 From: slhafzjw Date: Sun, 5 Oct 2025 14:28:07 +0800 Subject: [PATCH] =?UTF-8?q?=E5=9C=A8=20daemon=20=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E4=BA=86=E5=BC=82=E5=B8=B8=E5=A4=84=E7=90=86=E8=A1=8C=E4=B8=BA?= =?UTF-8?q?;=20=E4=B8=BA=E7=BC=96=E8=BE=91=E5=99=A8=E7=AD=89=E4=B9=9F?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E4=BA=86=E5=8F=AF=E5=AE=9A=E4=B9=89=E7=9A=84?= =?UTF-8?q?=E7=8E=AF=E5=A2=83=E5=8F=98=E9=87=8F;=20=E6=9B=B4=E6=96=B0=20RE?= =?UTF-8?q?ADME;?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/java/work/slhaf/snippet/App.java | 20 +++----------- .../exception/ActionHandleException.java | 12 +++++++++ .../exception/LaunchCheckException.java | 11 ++++++++ .../exception/SnippetManagerException.java | 11 ++++++++ .../slhaf/snippet/service/ActionHandler.java | 25 +++++++++++------- .../slhaf/snippet/service/IndexManager.java | 14 +++++++++- .../slhaf/snippet/service/SnippetManager.java | 5 ++-- CodeSnippetRofi/common/constant.py | 5 +++- CodeSnippetRofi/common/rofi.py | 4 +-- CodeSnippetRofi/helper/file_helper.py | 20 +++++++------- CodeSnippetRofi/launcher.py | 9 ++----- README.md | 24 ++++++++++++++--- doc/resource/dir.png | Bin 0 -> 11977 bytes 13 files changed, 109 insertions(+), 51 deletions(-) create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/exception/ActionHandleException.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/exception/LaunchCheckException.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/exception/SnippetManagerException.java create mode 100644 doc/resource/dir.png diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/App.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/App.java index ea476a9..e6208ab 100644 --- a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/App.java +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/App.java @@ -1,10 +1,8 @@ package work.slhaf.snippet; -import cn.hutool.core.bean.BeanUtil; import lombok.extern.slf4j.Slf4j; import work.slhaf.snippet.common.Constant; -import work.slhaf.snippet.entity.db.Index; -import work.slhaf.snippet.entity.file.RebuildEntity; +import work.slhaf.snippet.exception.LaunchCheckException; import work.slhaf.snippet.gateway.CodeSnippetSocketServer; import work.slhaf.snippet.service.IndexManager; import work.slhaf.snippet.service.SnippetManager; @@ -13,9 +11,6 @@ import java.io.File; import java.io.IOException; import java.sql.SQLException; import java.util.HashMap; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; @Slf4j public class App{ @@ -53,14 +48,7 @@ public class App{ HashMap sha2PathMD = snippetManager.getFileStatus(); if (!sha2PathMD.equals(sha2PathDB)) { log.info("数据库与文件目录不一致,重建索引数据库"); - List snippets = snippetManager.listAll(); - Set indexSet = snippets.stream().map(entity -> { - Index index = new Index(); - BeanUtil.copyProperties(entity, index); - return index; - }) - .collect(Collectors.toSet()); - indexManager.rebuildIndex(indexSet); + indexManager.rebuildIndex(); } log.info("索引数据库检查通过"); } @@ -82,7 +70,7 @@ public class App{ boolean ok = file.mkdirs(); if (!ok) { - throw new RuntimeException("创建目录失败: " + dir); + throw new LaunchCheckException("创建目录失败: " + dir); } else { log.info("创建目录成功: {}", dir); } @@ -91,7 +79,7 @@ public class App{ private String getEnvOrThrow(String key) { String value = System.getenv(key); - if (value == null) throw new RuntimeException("未找到环境变量: " + key); + if (value == null) throw new LaunchCheckException("未找到环境变量: " + key); return value; } diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/exception/ActionHandleException.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/exception/ActionHandleException.java new file mode 100644 index 0000000..2eb9525 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/exception/ActionHandleException.java @@ -0,0 +1,12 @@ +package work.slhaf.snippet.exception; + +public class ActionHandleException extends RuntimeException +{ + public ActionHandleException(String message) { + super(message); + } + + public ActionHandleException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/exception/LaunchCheckException.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/exception/LaunchCheckException.java new file mode 100644 index 0000000..bc8dcdc --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/exception/LaunchCheckException.java @@ -0,0 +1,11 @@ +package work.slhaf.snippet.exception; + +public class LaunchCheckException extends RuntimeException{ + public LaunchCheckException(String message) { + super(message); + } + + public LaunchCheckException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/exception/SnippetManagerException.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/exception/SnippetManagerException.java new file mode 100644 index 0000000..43b7462 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/exception/SnippetManagerException.java @@ -0,0 +1,11 @@ +package work.slhaf.snippet.exception; + +public class SnippetManagerException extends RuntimeException{ + public SnippetManagerException(String message) { + super(message); + } + + public SnippetManagerException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/ActionHandler.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/ActionHandler.java index bdb45b5..6c5b4aa 100644 --- a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/ActionHandler.java +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/ActionHandler.java @@ -12,6 +12,7 @@ import work.slhaf.snippet.entity.file.AddEntity; import work.slhaf.snippet.entity.file.EditEntity; import work.slhaf.snippet.entity.file.ListEntity; import work.slhaf.snippet.entity.file.MetaDataEntity; +import work.slhaf.snippet.exception.ActionHandleException; import java.io.IOException; import java.nio.file.Files; @@ -53,16 +54,22 @@ public class ActionHandler { private SocketOutputData handleDelete(String path) throws SQLException, IOException { if (path.isEmpty()) { - return new SocketOutputData(Constant.Status.FAILED, "Path不能为空!"); + throw new ActionHandleException("Path不能为空!"); + } + try { + indexManager.delete(path); + snippetManager.delete(path); + return new SocketOutputData(Constant.Status.SUCCESS, "删除成功: " + path); + } catch (Exception e) { + log.warn("代码片段删除失败,尝试重建索引"); + indexManager.rebuildIndex(); + throw new ActionHandleException("代码片段删除失败,路径: " + path, e); } - indexManager.delete(path); - snippetManager.delete(path); - return new SocketOutputData(Constant.Status.SUCCESS, "删除成功: " + path); } private SocketOutputData handleEdit(EditEntity entity) throws IOException { if (entity.checkEmpty()) { - return new SocketOutputData(Constant.Status.FAILED, "Id、Path、代码内容均不能为空!"); + throw new ActionHandleException("Id、Path、代码内容均不能为空!"); } try { snippetManager.update(entity, SnippetManager.UpdateAction.EDIT); @@ -75,14 +82,13 @@ public class ActionHandler { return new SocketOutputData(Constant.Status.SUCCESS, "文件编辑成功: " + entity.getPath()); } catch (Exception e) { snippetManager.update(entity, SnippetManager.UpdateAction.FALLBACK); - log.error("文件编辑失败, 已回滚: {}", e.getLocalizedMessage()); - return new SocketOutputData(Constant.Status.FAILED, e.getLocalizedMessage()); + throw new ActionHandleException("文件编辑失败,已回滚: " + entity, e); } } private SocketOutputData handleAdd(AddEntity entity) throws IOException { if (entity.checkEmpty()) { - return new SocketOutputData(Constant.Status.FAILED, "Language、Name、代码片段内容均不能为空!"); + throw new ActionHandleException("Language、Name、代码片段内容均不能为空!"); } Path path = Path.of(System.getenv(Constant.Property.DIR), entity.getLanguage().toLowerCase(), entity.getName() + ".md"); try { @@ -94,8 +100,7 @@ public class ActionHandler { return new SocketOutputData(Constant.Status.SUCCESS, "代码片段已添加, 路径: " + path); } catch (Exception e) { Files.deleteIfExists(path); - log.error("文件添加失败: {}", e.getLocalizedMessage()); - return new SocketOutputData(Constant.Status.FAILED, e.getLocalizedMessage()); + throw new ActionHandleException("文件添加失败: " + entity, e); } } diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/IndexManager.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/IndexManager.java index d9bded1..9bd3f0e 100644 --- a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/IndexManager.java +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/IndexManager.java @@ -1,12 +1,16 @@ package work.slhaf.snippet.service; +import cn.hutool.core.bean.BeanUtil; import lombok.extern.slf4j.Slf4j; import work.slhaf.snippet.common.Constant; import work.slhaf.snippet.entity.db.Index; import work.slhaf.snippet.entity.file.ListEntity; +import work.slhaf.snippet.entity.file.RebuildEntity; +import java.io.IOException; import java.sql.*; import java.util.*; +import java.util.stream.Collectors; @Slf4j public class IndexManager { @@ -14,6 +18,7 @@ public class IndexManager { private static volatile IndexManager instance; private final Connection connection; + private final SnippetManager snippetManager = new SnippetManager(); public static IndexManager getInstance() throws SQLException { if (instance == null) { @@ -47,9 +52,16 @@ public class IndexManager { statement.close(); } - public void rebuildIndex(Set indexSet) throws SQLException { + public void rebuildIndex() throws SQLException, IOException { log.info("重建索引数据库"); resetIndex(); + List snippets = snippetManager.listAll(); + Set indexSet = snippets.stream().map(entity -> { + Index index = new Index(); + BeanUtil.copyProperties(entity, index); + return index; + }) + .collect(Collectors.toSet()); for (Index index : indexSet) { add(index); } diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/SnippetManager.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/SnippetManager.java index 3816ada..b7bc4f0 100644 --- a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/SnippetManager.java +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/SnippetManager.java @@ -8,6 +8,7 @@ import work.slhaf.snippet.common.Constant; import work.slhaf.snippet.entity.Snippet; import work.slhaf.snippet.entity.file.EditEntity; import work.slhaf.snippet.entity.file.RebuildEntity; +import work.slhaf.snippet.exception.SnippetManagerException; import java.io.File; import java.io.FileReader; @@ -52,7 +53,7 @@ public class SnippetManager { String markdown = snippet.toMarkdown(); File file = path.toFile(); if (file.exists()) { - throw new RuntimeException("文件已存在: " + file.getAbsolutePath()); + throw new SnippetManagerException("文件已存在: " + file.getAbsolutePath()); } file.getParentFile().mkdirs(); Files.writeString(file.toPath(), markdown, StandardCharsets.UTF_8); @@ -102,7 +103,7 @@ public class SnippetManager { public String sha(String filePath) { File file = new File(filePath); if (!file.exists()) { - throw new RuntimeException("文件不存在: " + filePath); + throw new SnippetManagerException("文件不存在: " + filePath); } return DigestUtil.sha256Hex(file); } diff --git a/CodeSnippetRofi/common/constant.py b/CodeSnippetRofi/common/constant.py index ba86237..9d1a3b0 100644 --- a/CodeSnippetRofi/common/constant.py +++ b/CodeSnippetRofi/common/constant.py @@ -2,13 +2,16 @@ import os code_snippet_dir = os.getenv("CODE_SNIPPET_DIR") code_snippet_port = os.getenv("CODE_SNIPPET_PORT") -code_snippet_rofi = os.getenv("CODE_SNIPPET_ROFI") +code_snippet_rofi = os.getenv("CODE_SNIPPET_ROFI", "rofi") +code_snippet_editor = os.getenv("CODE_SNIPPET_EDITOR","nvim") action_list = "LIST" action_add = "ADD" action_edit = "EDIT" action_delete = "DELETE" +editor_class = "code_snippet_editor" + template_add = """ ## Snippet diff --git a/CodeSnippetRofi/common/rofi.py b/CodeSnippetRofi/common/rofi.py index 3ef9137..845d89d 100644 --- a/CodeSnippetRofi/common/rofi.py +++ b/CodeSnippetRofi/common/rofi.py @@ -25,11 +25,11 @@ # import atexit -from datetime import datetime -from decimal import Decimal, InvalidOperation import signal import subprocess import time +from datetime import datetime +from decimal import Decimal, InvalidOperation # Python < 3.2 doesn't provide a context manager interface for Popen. # Let's make our own wrapper if needed. diff --git a/CodeSnippetRofi/helper/file_helper.py b/CodeSnippetRofi/helper/file_helper.py index 0044c46..81171c7 100644 --- a/CodeSnippetRofi/helper/file_helper.py +++ b/CodeSnippetRofi/helper/file_helper.py @@ -8,6 +8,8 @@ import subprocess import tempfile from pathlib import Path +from CodeSnippetRofi.common.constant import editor_class, code_snippet_editor + from common.constant import template_add, template_edit from entity.result import ActionResult from helper.api_helper import run_edit, run_add @@ -201,23 +203,23 @@ def _open_with_nvim(file_path: str) -> None: # 根据终端类型添加特定参数 if name == 'alacritty': - cmd.extend(['--class', 'floating-nvim', '-e', 'nvim', file_path]) + cmd.extend(['--class', editor_class, '-e', code_snippet_editor, file_path]) elif name == 'kitty': - cmd.extend(['--class', 'floating-nvim', 'nvim', file_path]) + cmd.extend(['--class', editor_class, code_snippet_editor, file_path]) elif name == 'foot': - cmd.extend(['-a', 'floating-nvim', 'nvim', file_path]) + cmd.extend(['-a', editor_class, code_snippet_editor, file_path]) elif name == 'wezterm': - cmd.extend(['start', '--class', 'floating-nvim', '--', 'nvim', file_path]) + cmd.extend(['start', '--class', editor_class, '--', code_snippet_editor, file_path]) elif name.startswith('gnome-terminal'): - cmd.extend(['--class=floating-nvim', '--', 'nvim', file_path]) + cmd.extend([f'--class={editor_class}', '--', code_snippet_editor, file_path]) elif name == 'konsole': - cmd.extend(['--class', 'floating-nvim', '-e', 'nvim', file_path]) + cmd.extend(['--class', editor_class, '-e', code_snippet_editor, file_path]) elif name == 'urxvt': - cmd.extend(['-name', 'floating-nvim', '-e', 'nvim', file_path]) + cmd.extend(['-name', editor_class, '-e', code_snippet_editor, file_path]) elif name == 'xterm': - cmd.extend(['-class', 'floating-nvim', '-e', 'nvim', file_path]) + cmd.extend(['-class', editor_class, '-e', code_snippet_editor, file_path]) else: - cmd.extend(['-e', 'nvim', file_path]) + cmd.extend(['-e', code_snippet_editor, file_path]) # 阻塞执行命令,直到进程结束 subprocess.run(cmd) diff --git a/CodeSnippetRofi/launcher.py b/CodeSnippetRofi/launcher.py index a4d314a..83e9f2e 100755 --- a/CodeSnippetRofi/launcher.py +++ b/CodeSnippetRofi/launcher.py @@ -1,11 +1,6 @@ -import os - -from common.constant import code_snippet_rofi from common import rofi +from common.constant import code_snippet_rofi from menu.MainMenu import MainMenu -if code_snippet_rofi is None: - r = rofi.Rofi() -else: - r = rofi.Rofi(code_snippet_rofi) +r = rofi.Rofi(code_snippet_rofi) MainMenu(r).run() diff --git a/README.md b/README.md index 16efde9..a0dc03b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# CodeSnippetDaemon +~~# CodeSnippet 使用 Rofi 前端 + Java 守护进程管理代码片段,支持 Markdown 存储、跨平台同步和快速索引。 ## 项目概述 @@ -26,9 +26,17 @@ ### 前端 - 默认使用 Rofi 作为启动器,已涵盖后端所需的所有操作类型 - 借助 Python 完成复杂操作 -- 涉及到代码片段的编辑行为时,将通过调用 nvim 来编辑临时文件,编辑完毕后将发送请求至守护进程 +- 涉及到代码片段的编辑行为时,默认将通过调用 nvim 来编辑临时文件,编辑完毕后将发送请求至守护进程 -## 使用方法 +## 快速开始 +### 安装 +1. 下载RELEASE +2. 下载所需库 + - `pip install python-rofi` +2. 解压,目录结构如下: + > ![目录结构](doc/resource/dir.png) + +### 使用 1. 设置环境变量 - CODE_SNIPPET_CONF - 配置文件目录 @@ -42,11 +50,21 @@ - AI服务商提供的 base_url - CODE_SNIPPET_MODEL - 所用模型名称 + - CODE_SNIPPET_EDITOR (可选,若未指定则使用nvim) + - 所用的编辑器,默认使用nvim + - Typora 好像也可以编辑,只是会额外打开一个终端窗口 + - 其他编辑器未测试 - CODE_SNIPPET_ROFI (可选,若未指定则使用默认 rofi) - 指定的 rofi 脚本,可以添加参数,比如: `rofi -theme ~/.config/rofi/launchers/type-4/style-1.rasi` 2. 启动 Java 守护进程 + - `java -jar daemon/CodeSnippetDaemon-1.0.jar` 3. 启动 rofi 脚本 + - `python rofi/launcher.py` + +> 已将打开的编辑器窗口的class属性设置为`code_snippet_editor`,如果使用的是 Hyprland 等 wm,可以据此对打开的编辑器窗口设置所需规则 +> +> 该部分只针对一些常见的终端应用进行了设置,如有需要,可以自行编辑文件: [file_helper(第205行开始)](CodeSnippetRofi/helper/file_helper.py) ## 说明 diff --git a/doc/resource/dir.png b/doc/resource/dir.png new file mode 100644 index 0000000000000000000000000000000000000000..c776c548fc434f15cac4d28eb7af776821bcee6b GIT binary patch literal 11977 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