From a6b2905ad25c65ea7f57eba1e17f1491a7968436 Mon Sep 17 00:00:00 2001 From: slhafzjw Date: Sun, 5 Oct 2025 00:30:37 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=A3=E7=A0=81=E7=89=87=E6=AE=B5=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=B7=A5=E5=85=B7=EF=BC=9Arofi=E5=89=8D=E7=AB=AF+Java?= =?UTF-8?q?=E5=AE=88=E6=8A=A4=E8=BF=9B=E7=A8=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 8 + .../src/main/java/work/slhaf/snippet/App.java | 98 +++ .../work/slhaf/snippet/common/Constant.java | 22 + .../slhaf/snippet/common/SnippetUtil.java | 13 + .../slhaf/snippet/common/chat/ChatClient.java | 70 ++ .../common/chat/constant/ChatConstant.java | 15 + .../snippet/common/chat/pojo/ChatBody.java | 25 + .../common/chat/pojo/ChatResponse.java | 16 + .../snippet/common/chat/pojo/Message.java | 15 + .../common/chat/pojo/PrimaryChatResponse.java | 111 +++ .../work/slhaf/snippet/entity/Snippet.java | 42 + .../slhaf/snippet/entity/SocketInputData.java | 10 + .../snippet/entity/SocketOutputData.java | 12 + .../work/slhaf/snippet/entity/db/Index.java | 14 + .../slhaf/snippet/entity/file/AddEntity.java | 14 + .../slhaf/snippet/entity/file/EditEntity.java | 20 + .../slhaf/snippet/entity/file/ListEntity.java | 16 + .../snippet/entity/file/MetaDataEntity.java | 9 + .../snippet/entity/file/RebuildEntity.java | 15 + .../slhaf/snippet/gateway/ClientSocket.java | 54 ++ .../gateway/CodeSnippetSocketServer.java | 34 + .../slhaf/snippet/service/ActionHandler.java | 120 +++ .../slhaf/snippet/service/IndexManager.java | 178 ++++ .../snippet/service/MetaDataExtractor.java | 55 ++ .../slhaf/snippet/service/SnippetManager.java | 144 +++ .../slhaf/snippet/service/SnippetReader.java | 134 +++ .../src/test/java/MarkdownTest.java | 11 + .../src/test/java/SnippetReaderTest.java | 29 + CodeSnippetRofi/common/constant.py | 40 + CodeSnippetRofi/common/rofi.py | 823 ++++++++++++++++++ CodeSnippetRofi/entity/response.py | 19 + CodeSnippetRofi/entity/result.py | 4 + CodeSnippetRofi/helper/api_helper.py | 58 ++ CodeSnippetRofi/helper/file_helper.py | 334 +++++++ CodeSnippetRofi/launcher.py | 11 + CodeSnippetRofi/menu/AddMenu.py | 13 + CodeSnippetRofi/menu/DeleteMenu.py | 44 + CodeSnippetRofi/menu/EditMenu.py | 32 + CodeSnippetRofi/menu/MainMenu.py | 29 + CodeSnippetRofi/menu/SearchMenu.py | 71 ++ LICENSE | 21 + README.md | 75 ++ doc/resource/add.png | Bin 0 -> 76705 bytes doc/resource/empty-add.png | Bin 0 -> 56274 bytes doc/接口说明.md | 81 ++ doc/模板说明.md | 34 + test/data-test.json | 8 + test/test.json | 7 + test/test.md | 50 ++ 49 files changed, 3058 insertions(+) create mode 100644 .gitignore create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/App.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/Constant.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/SnippetUtil.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/ChatClient.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/constant/ChatConstant.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/ChatBody.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/ChatResponse.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/Message.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/PrimaryChatResponse.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/Snippet.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/SocketInputData.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/SocketOutputData.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/db/Index.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/AddEntity.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/EditEntity.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/ListEntity.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/MetaDataEntity.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/RebuildEntity.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/gateway/ClientSocket.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/gateway/CodeSnippetSocketServer.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/ActionHandler.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/IndexManager.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/MetaDataExtractor.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/SnippetManager.java create mode 100644 CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/SnippetReader.java create mode 100644 CodeSnippetDaemon/src/test/java/MarkdownTest.java create mode 100644 CodeSnippetDaemon/src/test/java/SnippetReaderTest.java create mode 100644 CodeSnippetRofi/common/constant.py create mode 100644 CodeSnippetRofi/common/rofi.py create mode 100644 CodeSnippetRofi/entity/response.py create mode 100644 CodeSnippetRofi/entity/result.py create mode 100644 CodeSnippetRofi/helper/api_helper.py create mode 100644 CodeSnippetRofi/helper/file_helper.py create mode 100755 CodeSnippetRofi/launcher.py create mode 100644 CodeSnippetRofi/menu/AddMenu.py create mode 100644 CodeSnippetRofi/menu/DeleteMenu.py create mode 100644 CodeSnippetRofi/menu/EditMenu.py create mode 100644 CodeSnippetRofi/menu/MainMenu.py create mode 100644 CodeSnippetRofi/menu/SearchMenu.py create mode 100644 LICENSE create mode 100644 README.md create mode 100644 doc/resource/add.png create mode 100644 doc/resource/empty-add.png create mode 100644 doc/接口说明.md create mode 100644 doc/模板说明.md create mode 100644 test/data-test.json create mode 100644 test/test.json create mode 100644 test/test.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6ad0f1a --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/CodeSnippetRofi/.idea/ +/CodeSnippetRofi/.venv/ +/CodeSnippetDaemon/.idea/ +/CodeSnippetDaemon/.mvn/ +/CodeSnippetDaemon/target/ +/CodeSnippetDaemon/dependency-reduced-pom.xml +/CodeSnippetDaemon/pom.xml +/.idea/ diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/App.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/App.java new file mode 100644 index 0000000..ea476a9 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/App.java @@ -0,0 +1,98 @@ +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.gateway.CodeSnippetSocketServer; +import work.slhaf.snippet.service.IndexManager; +import work.slhaf.snippet.service.SnippetManager; + +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{ + + void main() throws IOException, SQLException { + beforeLaunch(); + CodeSnippetSocketServer server = new CodeSnippetSocketServer(); + server.launch(); + } + + private void beforeLaunch() throws SQLException, IOException { + log.info("启动前检查环境"); + //检查对应目录是否存在并创建 + checkEnv(); + checkDB(); + } + + private void checkDB() throws SQLException, IOException { + log.info("检查索引数据库"); + //获取当前数据路径下的文件目录,查看文件路径、sha值与索引数据库是否一致,如果不一致需要重建 + IndexManager indexManager = IndexManager.getInstance(); + SnippetManager snippetManager = new SnippetManager(); + + File data = new File(System.getenv(Constant.Property.DIR)); + File[] files = data.listFiles(); + if (files == null || files.length == 0) { + if (!indexManager.isEmpty()) { + log.info("数据库为空,但文件目录不为空,删除数据库"); + indexManager.resetIndex(); + } + return; + } + + HashMap sha2PathDB = indexManager.getIndexStatus(); + 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); + } + log.info("索引数据库检查通过"); + } + + private void checkEnv() { + log.info("检查环境变量"); + getEnvOrThrow(Constant.Property.PORT); + getEnvOrThrow(Constant.Property.API_KEY); + getEnvOrThrow(Constant.Property.BASE_URL); + getEnvOrThrow(Constant.Property.MODEL); + checkDir(Constant.Property.DIR); + checkDir(Constant.Property.CONF); + log.info("环境变量检查通过"); + } + + private void checkDir(String dir) { + File file = new File(getEnvOrThrow(dir)); + if (file.exists()) return; + + boolean ok = file.mkdirs(); + if (!ok) { + throw new RuntimeException("创建目录失败: " + dir); + } else { + log.info("创建目录成功: {}", dir); + } + + } + + private String getEnvOrThrow(String key) { + String value = System.getenv(key); + if (value == null) throw new RuntimeException("未找到环境变量: " + key); + return value; + } + +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/Constant.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/Constant.java new file mode 100644 index 0000000..9cd27f2 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/Constant.java @@ -0,0 +1,22 @@ +package work.slhaf.snippet.common; + +public class Constant { + + public static class Property { + private static final String BASE = "CODE_SNIPPET_"; + public static final String DIR = BASE + "DIR"; + public static final String CONF = BASE + "CONF"; + public static final String PORT = BASE + "PORT"; + public static final String API_KEY = BASE + "API_KEY"; + public static final String BASE_URL = BASE + "BASE_URL"; + public static final String MODEL = BASE + "MODEL"; + } + + public enum Action { + LIST, EDIT, ADD, DELETE + } + + public enum Status { + SUCCESS, FAILED + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/SnippetUtil.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/SnippetUtil.java new file mode 100644 index 0000000..0c0303e --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/SnippetUtil.java @@ -0,0 +1,13 @@ +package work.slhaf.snippet.common; + +public class SnippetUtil { + public static String extractJson(String jsonStr) { + jsonStr = jsonStr.replace("“", "\"").replace("”", "\""); + int start = jsonStr.indexOf("{"); + int end = jsonStr.lastIndexOf("}"); + if (start != -1 && end != -1 && start < end) { + return jsonStr.substring(start, end + 1); + } + return jsonStr; + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/ChatClient.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/ChatClient.java new file mode 100644 index 0000000..ff9ef37 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/ChatClient.java @@ -0,0 +1,70 @@ +package work.slhaf.snippet.common.chat; + +import cn.hutool.http.HttpRequest; +import cn.hutool.http.HttpResponse; +import cn.hutool.json.JSONUtil; +import lombok.Data; +import lombok.NoArgsConstructor; +import work.slhaf.snippet.common.chat.constant.ChatConstant; +import work.slhaf.snippet.common.chat.pojo.ChatBody; +import work.slhaf.snippet.common.chat.pojo.ChatResponse; +import work.slhaf.snippet.common.chat.pojo.Message; +import work.slhaf.snippet.common.chat.pojo.PrimaryChatResponse; + +import java.util.List; + +@Data +@NoArgsConstructor +public class ChatClient { + private String clientId; + + private String url; + private String apikey; + private String model; + + private double top_p; + private double temperature; + private int max_tokens; + + public ChatClient(String url, String apikey, String model) { + this.url = url; + this.apikey = apikey; + this.model = model; + } + + public ChatResponse runChat(List messages) { + HttpRequest request = HttpRequest.post(url); + request.header("Content-Type", "application/json"); + request.header("Authorization", "Bearer " + apikey); + + ChatBody body; + if (top_p > 0) { + body = ChatBody.builder() + .model(model) + .messages(messages) + .top_p(top_p) + .temperature(temperature) + .max_tokens(max_tokens) + .build(); + } else { + body = ChatBody.builder() + .model(model) + .messages(messages) + .build(); + } + + HttpResponse response = request.body(JSONUtil.toJsonStr(body)).execute(); + ChatResponse finalResponse; + + PrimaryChatResponse primaryChatResponse = JSONUtil.toBean(response.body(), PrimaryChatResponse.class); + finalResponse = ChatResponse.builder() + .type(ChatConstant.Response.SUCCESS) + .message(primaryChatResponse.getChoices().get(0).getMessage().getContent()) + .usageBean(primaryChatResponse.getUsage()) + .build(); + + response.close(); + return finalResponse; + } + +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/constant/ChatConstant.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/constant/ChatConstant.java new file mode 100644 index 0000000..6516da1 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/constant/ChatConstant.java @@ -0,0 +1,15 @@ +package work.slhaf.snippet.common.chat.constant; + +public class ChatConstant { + + public static class Character { + public static final String USER = "user"; + public static final String SYSTEM = "system"; + public static final String ASSISTANT = "assistant"; + } + + public static class Response { + public static final String SUCCESS = "success"; + public static final String ERROR = "error"; + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/ChatBody.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/ChatBody.java new file mode 100644 index 0000000..ab18f37 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/ChatBody.java @@ -0,0 +1,25 @@ +package work.slhaf.snippet.common.chat.pojo; + +import lombok.*; + +import java.util.List; + +@Builder +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatBody { + @NonNull + private String model; + @NonNull + private List messages; + @Builder.Default + private double temperature = 1; + @Builder.Default + private double top_p = 1; + private boolean stream; + @Builder.Default + private int max_tokens = 1024; + private int presence_penalty; + private int frequency_penalty; +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/ChatResponse.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/ChatResponse.java new file mode 100644 index 0000000..0de2e90 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/ChatResponse.java @@ -0,0 +1,16 @@ +package work.slhaf.snippet.common.chat.pojo; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ChatResponse { + private String type; + private String message; + private PrimaryChatResponse.UsageBean usageBean; +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/Message.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/Message.java new file mode 100644 index 0000000..7d6cc7e --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/Message.java @@ -0,0 +1,15 @@ +package work.slhaf.snippet.common.chat.pojo; + +import lombok.*; + +@Builder +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Message { + + @NonNull + private String role; + @NonNull + private String content; +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/PrimaryChatResponse.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/PrimaryChatResponse.java new file mode 100644 index 0000000..7e3ec4b --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/common/chat/pojo/PrimaryChatResponse.java @@ -0,0 +1,111 @@ +package work.slhaf.snippet.common.chat.pojo; + +import lombok.Getter; +import lombok.Setter; + +import java.util.List; + +@Getter +@Setter +public class PrimaryChatResponse { + + /** + * id + */ + private String id; + /** + * object + */ + private String object; + /** + * created + */ + private int created; + /** + * model + */ + private String model; + /** + * choices + */ + private List choices; + /** + * usage + */ + private UsageBean usage; + /** + * system_fingerprint + */ + private String system_fingerprint; + + @Setter + @Getter + public static class UsageBean { + /** + * prompt_tokens + */ + private int prompt_tokens; + /** + * completion_tokens + */ + private int completion_tokens; + /** + * total_tokens + */ + private int total_tokens; + /** + * prompt_cache_hit_tokens + */ + private int prompt_cache_hit_tokens; + /** + * prompt_cache_miss_tokens + */ + private int prompt_cache_miss_tokens; + + @Override + public String toString() { + return "UsageBean{" + + "prompt_tokens=" + prompt_tokens + + ", completion_tokens=" + completion_tokens + + ", total_tokens=" + total_tokens + + ", prompt_cache_hit_tokens=" + prompt_cache_hit_tokens + + ", prompt_cache_miss_tokens=" + prompt_cache_miss_tokens + + '}'; + } + } + + @Setter + @Getter + public static class ChoicesBean { + /** + * index + */ + private int index; + /** + * message + */ + private MessageBean message; + /** + * logprobs + */ + private Object logprobs; + /** + * finish_reason + */ + private String finish_reason; + + @Setter + @Getter + public static class MessageBean { + /** + * role + */ + private String role; + /** + * content + */ + private String content; + + } + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/Snippet.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/Snippet.java new file mode 100644 index 0000000..4212dbe --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/Snippet.java @@ -0,0 +1,42 @@ +package work.slhaf.snippet.entity; + +import lombok.Data; + +/** + * 片段完整数据,存储至完整的markdown文件中 + * 而数据库条目只需要元信息即可 + */ +@Data +public class Snippet { + + private String language; + private String[] tags; + private String description; + + private String content; + + public String toMarkdown() { + StringBuilder sb = new StringBuilder(); + + // Add Snippet content + sb.append("## Snippet\n"); + sb.append("```").append(language).append("\n"); + sb.append(content).append("\n"); + sb.append("```\n\n"); + + // Add MetaData section + sb.append("## MetaData\n"); + sb.append("- Language\n"); + sb.append(" - ").append(language).append("\n"); + + sb.append("- Tags\n"); + for (String tag : tags) { + sb.append(" - ").append(tag).append("\n"); + } + + sb.append("- Description\n"); + sb.append(" - ").append(description).append("\n"); + + return sb.toString(); + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/SocketInputData.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/SocketInputData.java new file mode 100644 index 0000000..b3cdcbb --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/SocketInputData.java @@ -0,0 +1,10 @@ +package work.slhaf.snippet.entity; + +import lombok.Data; +import work.slhaf.snippet.common.Constant; + +@Data +public class SocketInputData { + private Constant.Action action; + private String data; +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/SocketOutputData.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/SocketOutputData.java new file mode 100644 index 0000000..cb08328 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/SocketOutputData.java @@ -0,0 +1,12 @@ +package work.slhaf.snippet.entity; + +import lombok.AllArgsConstructor; +import lombok.Data; +import work.slhaf.snippet.common.Constant; + +@Data +@AllArgsConstructor +public class SocketOutputData { + private Constant.Status status; + private String data; +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/db/Index.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/db/Index.java new file mode 100644 index 0000000..8514838 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/db/Index.java @@ -0,0 +1,14 @@ +package work.slhaf.snippet.entity.db; + +import lombok.Data; + +@Data +public class Index { + private int id; + private String name; + private String language; + private String[] tags; + private String sha; + private String description; + private String path; +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/AddEntity.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/AddEntity.java new file mode 100644 index 0000000..f636128 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/AddEntity.java @@ -0,0 +1,14 @@ +package work.slhaf.snippet.entity.file; + +import lombok.Data; + +@Data +public class AddEntity { + private String name; + private String language; + private String content; + + public boolean checkEmpty(){ + return name.isEmpty() || language.isEmpty() || content.isEmpty(); + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/EditEntity.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/EditEntity.java new file mode 100644 index 0000000..a9ff58d --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/EditEntity.java @@ -0,0 +1,20 @@ +package work.slhaf.snippet.entity.file; + +import lombok.Data; + +@Data +public class EditEntity { + private String id; + private String path; + + private String[] tags; + private String description; + + private String content; + + public boolean checkEmpty() { + return id.isEmpty() || + path.isEmpty() || + content.isEmpty(); + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/ListEntity.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/ListEntity.java new file mode 100644 index 0000000..11bf969 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/ListEntity.java @@ -0,0 +1,16 @@ +package work.slhaf.snippet.entity.file; + +import lombok.Data; + +@Data +public class ListEntity implements Comparable { + private String id; + private String path; + private String name; + private int score; + + @Override + public int compareTo(ListEntity listEntity) { + return Integer.compare(listEntity.getScore(), this.score); + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/MetaDataEntity.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/MetaDataEntity.java new file mode 100644 index 0000000..dfbaf61 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/MetaDataEntity.java @@ -0,0 +1,9 @@ +package work.slhaf.snippet.entity.file; + +import lombok.Data; + +@Data +public class MetaDataEntity { + private String description; + private String[] tags; +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/RebuildEntity.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/RebuildEntity.java new file mode 100644 index 0000000..d11eb6d --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/entity/file/RebuildEntity.java @@ -0,0 +1,15 @@ +package work.slhaf.snippet.entity.file; + +import lombok.Data; + +@Data +public class RebuildEntity { + private String name; + private String path; + private String sha; + + private String language; + private String[] tags; + private String description; + +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/gateway/ClientSocket.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/gateway/ClientSocket.java new file mode 100644 index 0000000..5a7c014 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/gateway/ClientSocket.java @@ -0,0 +1,54 @@ +package work.slhaf.snippet.gateway; + +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; +import work.slhaf.snippet.common.Constant; +import work.slhaf.snippet.entity.SocketInputData; +import work.slhaf.snippet.entity.SocketOutputData; +import work.slhaf.snippet.service.ActionHandler; + +import java.io.*; +import java.net.Socket; + +@Slf4j +public record ClientSocket(Socket socket, ActionHandler handler) implements Runnable { + + @Override + public void run() { + System.out.println("建立连接: " + socket.getRemoteSocketAddress()); + try (BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream())); + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) { + + StringBuilder sb = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + sb.append(line); + } + + SocketInputData inputData = JSONUtil.toBean(sb.toString(), SocketInputData.class); + SocketOutputData outputData = handler.handle(inputData); + + writer.write(JSONUtil.toJsonStr(outputData)); + writer.newLine(); + writer.flush(); + + } catch (Exception e) { + SocketOutputData outputData = new SocketOutputData(Constant.Status.FAILED, e.getLocalizedMessage()); + try { + BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream())); + writer.write(JSONUtil.toJsonStr(outputData)); + writer.newLine(); + writer.flush(); + } catch (IOException ex) { + log.error(ex.getLocalizedMessage()); + } + } finally { + try { + socket.close(); + } catch (IOException e) { + log.error(e.getLocalizedMessage()); + } + } + } + +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/gateway/CodeSnippetSocketServer.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/gateway/CodeSnippetSocketServer.java new file mode 100644 index 0000000..9cbe4c1 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/gateway/CodeSnippetSocketServer.java @@ -0,0 +1,34 @@ +package work.slhaf.snippet.gateway; + +import lombok.extern.slf4j.Slf4j; +import work.slhaf.snippet.common.Constant; +import work.slhaf.snippet.service.ActionHandler; + +import java.io.IOException; +import java.net.ServerSocket; +import java.sql.SQLException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +@Slf4j +public class CodeSnippetSocketServer { + + public void launch() throws IOException, SQLException { + int port = Integer.parseInt(System.getenv(Constant.Property.PORT)); + ActionHandler handler = new ActionHandler(); + try (ServerSocket socket = new ServerSocket(port); + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) { + socket.setReuseAddress(true); + while (true) { + Future future = executor.submit(new ClientSocket(socket.accept(), handler)); + try{ + future.get(); + }catch (Exception e){ + log.error(e.getLocalizedMessage()); + } + } + } + } + +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/ActionHandler.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/ActionHandler.java new file mode 100644 index 0000000..bdb45b5 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/ActionHandler.java @@ -0,0 +1,120 @@ +package work.slhaf.snippet.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.json.JSONUtil; +import lombok.extern.slf4j.Slf4j; +import work.slhaf.snippet.common.Constant; +import work.slhaf.snippet.entity.Snippet; +import work.slhaf.snippet.entity.SocketInputData; +import work.slhaf.snippet.entity.SocketOutputData; +import work.slhaf.snippet.entity.db.Index; +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 java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.SQLException; +import java.util.List; + +@Slf4j +public class ActionHandler { + + private final SnippetManager snippetManager = new SnippetManager(); + private final IndexManager indexManager = IndexManager.getInstance(); + private final MetaDataExtractor metaDataExtractor = new MetaDataExtractor(); + + public ActionHandler() throws SQLException { + } + + public SocketOutputData handle(SocketInputData inputData) { + log.info("收到请求: {}", inputData.getAction()); + String data = inputData.getData(); + try { + return switch (inputData.getAction()) { + case Constant.Action.ADD -> handleAdd(JSONUtil.toBean(data, AddEntity.class)); + case Constant.Action.EDIT -> handleEdit(JSONUtil.toBean(data, EditEntity.class)); + case Constant.Action.DELETE -> handleDelete(data); + case Constant.Action.LIST -> handleList(data); + }; + } catch (Exception e) { + log.error("处理请求失败: {}", e.getLocalizedMessage()); + return new SocketOutputData(Constant.Status.FAILED, e.getLocalizedMessage()); + } + + } + + private SocketOutputData handleList(String key) throws SQLException { + List result = indexManager.list(key); + return new SocketOutputData(Constant.Status.SUCCESS, JSONUtil.toJsonStr(result)); + } + + private SocketOutputData handleDelete(String path) throws SQLException, IOException { + if (path.isEmpty()) { + return new SocketOutputData(Constant.Status.FAILED, "Path不能为空!"); + } + 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、代码内容均不能为空!"); + } + try { + snippetManager.update(entity, SnippetManager.UpdateAction.EDIT); + Index index = new Index(); + BeanUtil.copyProperties(entity, index); + String sha = snippetManager.sha(entity.getPath()); + index.setSha(sha); + indexManager.update(index); + snippetManager.update(entity, SnippetManager.UpdateAction.CONFIRM); + 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()); + } + } + + private SocketOutputData handleAdd(AddEntity entity) throws IOException { + if (entity.checkEmpty()) { + return new SocketOutputData(Constant.Status.FAILED, "Language、Name、代码片段内容均不能为空!"); + } + Path path = Path.of(System.getenv(Constant.Property.DIR), entity.getLanguage().toLowerCase(), entity.getName() + ".md"); + try { + Snippet snippet = fixSnippet(entity); + snippetManager.add(snippet, path); + Index index = fixIndexEntity(snippet, path); + index.setName(entity.getName()); + indexManager.add(index); + 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()); + } + } + + private Index fixIndexEntity(Snippet snippet, Path path) { + Index index = new Index(); + BeanUtil.copyProperties(snippet, index); + index.setPath(path.toString()); + index.setSha(snippetManager.sha(index.getPath())); + return index; + } + + private Snippet fixSnippet(AddEntity entity) { + MetaDataEntity metaData = metaDataExtractor.extract(entity); + Snippet snippet = new Snippet(); + snippet.setLanguage(entity.getLanguage().toLowerCase()); + snippet.setContent(entity.getContent()); + snippet.setDescription(metaData.getDescription()); + snippet.setTags(metaData.getTags()); + + return snippet; + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/IndexManager.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/IndexManager.java new file mode 100644 index 0000000..d9bded1 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/IndexManager.java @@ -0,0 +1,178 @@ +package work.slhaf.snippet.service; + +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 java.sql.*; +import java.util.*; + +@Slf4j +public class IndexManager { + + private static volatile IndexManager instance; + + private final Connection connection; + + public static IndexManager getInstance() throws SQLException { + if (instance == null) { + synchronized (IndexManager.class) { + if (instance == null) { + instance = new IndexManager(); + } + } + } + return instance; + } + + private IndexManager() throws SQLException { + connection = DriverManager.getConnection("jdbc:sqlite:" + System.getenv(Constant.Property.CONF) + "/index.db"); + checkTable(connection); + } + + private void checkTable(Connection connection) throws SQLException { + Statement statement = connection.createStatement(); + statement.execute(""" + create table if not exists code_snippet_index( + id integer primary key, + name varchar(255) not null, + language varchar(60) not null, + tags varchar(300) not null, + sha varchar(100) not null, + description varchar(300) not null, + path varchar(300) not null + ) + """); + statement.close(); + } + + public void rebuildIndex(Set indexSet) throws SQLException { + log.info("重建索引数据库"); + resetIndex(); + for (Index index : indexSet) { + add(index); + } + } + + public void add(Index index) throws SQLException { + PreparedStatement statement = connection.prepareStatement(""" + insert into code_snippet_index(name,language, tags, sha, description, path) + values (?,?,?,?,?,?) + """); + statement.setString(1, index.getName()); + statement.setString(2, index.getLanguage()); + statement.setString(3, Arrays.toString(index.getTags())); + statement.setString(4, index.getSha()); + statement.setString(5, index.getDescription()); + statement.setString(6, index.getPath()); + statement.execute(); + statement.close(); + } + + public void update(Index index) throws SQLException { + PreparedStatement statement = connection.prepareStatement(""" + update code_snippet_index + set tags = ?, description = ?, sha = ? + where id = ? + """); + statement.setString(1, Arrays.toString(index.getTags())); + statement.setString(2, index.getDescription()); + statement.setString(3, index.getSha()); + statement.executeUpdate(); + statement.close(); + } + + public void delete(String path) throws SQLException { + PreparedStatement statement = connection.prepareStatement("delete from code_snippet_index where path = ?"); + statement.setString(1, path); + statement.executeUpdate(); + } + + public List list(String input) throws SQLException { + String[] keywords = input.trim().toLowerCase().split("\\s+"); + + // 构建 SQL 占位符字符串 + StringBuilder sb = new StringBuilder("SELECT * FROM code_snippet_index WHERE "); + List params = new ArrayList<>(); + for (int k = 0; k < keywords.length; k++) { + if (k > 0) sb.append(" OR "); + sb.append("(name LIKE ? OR description LIKE ? OR tags LIKE ? OR language LIKE ?)"); + for (int i = 0; i < 4; i++) params.add("%" + keywords[k] + "%"); + } + + PreparedStatement statement = connection.prepareStatement(sb.toString()); + for (int i = 0; i < params.size(); i++) { + statement.setString(i + 1, params.get(i)); + } + + ResultSet rs = statement.executeQuery(); + List list = new ArrayList<>(); + + while (rs.next()) { + ListEntity entity = new ListEntity(); + entity.setId(rs.getString("id")); + entity.setPath(rs.getString("path")); + entity.setName(rs.getString("name")); + entity.setScore(calculateScore(rs, keywords)); // 多关键字打分 + list.add(entity); + } + + // 按分数降序排序 + list.sort(Comparator.comparingInt(ListEntity::getScore).reversed()); + return list; + } + + // 多关键字加权打分 + private int calculateScore(ResultSet rs, String[] keywords) throws SQLException { + int score = 0; + String name = rs.getString("name").toLowerCase(); + String description = rs.getString("description").toLowerCase(); + String language = rs.getString("language").toLowerCase(); + + String tagsRaw = rs.getString("tags"); + List tagList = Arrays.stream(tagsRaw.substring(1, tagsRaw.length() - 1) + .split(",")) + .map(String::toLowerCase) + .toList(); + + for (String key : keywords) { + if (name.contains(key)) score += 5; + if (tagList.stream().anyMatch(t -> t.contains(key))) score += 4; + if (description.contains(key)) score += 3; + if (language.contains(key)) score += 2; + } + return score; + } + + + public boolean isEmpty() throws SQLException { + Statement statement = connection.createStatement(); + //判断表是否为空 + ResultSet rs = statement.executeQuery("select count(*) from code_snippet_index"); + rs.next(); + int count = rs.getInt(1); + statement.close(); + return count == 0; + } + + public void resetIndex() throws SQLException { + log.info("重置索引"); + //删除表中全部索引条目 + Statement statement = connection.createStatement(); + statement.execute("delete from code_snippet_index"); + statement.close(); + } + + public HashMap getIndexStatus() throws SQLException { + Statement statement = connection.createStatement(); + ResultSet rs = statement.executeQuery(""" + select sha,path from code_snippet_index + """); + HashMap map = new HashMap<>(); + while (rs.next()) { + map.put(rs.getString("path"), rs.getString("sha")); + } + return map; + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/MetaDataExtractor.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/MetaDataExtractor.java new file mode 100644 index 0000000..d41d980 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/MetaDataExtractor.java @@ -0,0 +1,55 @@ +package work.slhaf.snippet.service; + +import cn.hutool.json.JSONUtil; +import work.slhaf.snippet.common.Constant; +import work.slhaf.snippet.common.chat.ChatClient; +import work.slhaf.snippet.common.chat.constant.ChatConstant; +import work.slhaf.snippet.common.chat.pojo.ChatResponse; +import work.slhaf.snippet.common.chat.pojo.Message; +import work.slhaf.snippet.entity.file.AddEntity; +import work.slhaf.snippet.entity.file.MetaDataEntity; + +import java.util.ArrayList; +import java.util.List; + +import static work.slhaf.snippet.common.SnippetUtil.extractJson; + +public class MetaDataExtractor { + private final ChatClient chatClient = new ChatClient( + System.getenv(Constant.Property.BASE_URL), + System.getenv(Constant.Property.API_KEY), + System.getenv(Constant.Property.MODEL) + ); + private final List messages = List.of( + new Message(ChatConstant.Character.SYSTEM, """ + 你需要对接下来发送的代码相关内容进行提取,提取出‘描述’、‘标签’等内容。 + + 输入格式示例: + ```json + { + "name": "ByteBuddy用法示例", //代码片段名称 + "language": "Java", //代码片段所属语言 + "content": "Class clazz = module.getClazz();\\nClass proxyClass = new ByteBuddy()\\n .subclass(clazz)\\n .method(ElementMatchers.isOverriddenFrom(overrideSource))\\n .intercept(MethodDelegation.to(new ModuleProxyInterceptor(record.post, record.pre)))\\n .make()\\n .load(ModuleProxyFactory.class.getClassLoader())\\n .getLoaded();" //代码片段内容 + } + ``` + + 输出格式示例 + ```json + { + "tags": [ + "ByteBuddy", + "动态代理" + ], + "description": "通过ByteBuddy创建动态代理类的示例" + } + ``` + """) + ); + + public MetaDataEntity extract(AddEntity entity) { + List temp = new ArrayList<>(messages); + temp.add(new Message(ChatConstant.Character.USER, JSONUtil.toJsonStr(entity))); + ChatResponse response = chatClient.runChat(temp); + return JSONUtil.toBean(extractJson(response.getMessage()), MetaDataEntity.class); + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/SnippetManager.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/SnippetManager.java new file mode 100644 index 0000000..3816ada --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/SnippetManager.java @@ -0,0 +1,144 @@ +package work.slhaf.snippet.service; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.core.io.file.FileNameUtil; +import cn.hutool.crypto.digest.DigestUtil; +import lombok.extern.slf4j.Slf4j; +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 java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +@Slf4j +public class SnippetManager { + + private final SnippetReader snippetReader = new SnippetReader(); + + public HashMap getFileStatus() { + File file = new File(System.getenv(Constant.Property.DIR)); + HashMap map = new HashMap<>(); + listFileStatus(file, map); + return map; + } + + private void listFileStatus(File file, HashMap map) { + File[] files = file.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (f.isDirectory()) { + listFileStatus(f, map); + } else { + String sha = DigestUtil.sha256Hex(f); + map.put(f.getAbsolutePath(), sha); + } + } + } + + public void add(Snippet snippet, Path path) throws IOException { + String markdown = snippet.toMarkdown(); + File file = path.toFile(); + if (file.exists()) { + throw new RuntimeException("文件已存在: " + file.getAbsolutePath()); + } + file.getParentFile().mkdirs(); + Files.writeString(file.toPath(), markdown, StandardCharsets.UTF_8); + } + + public void update(EditEntity entity, UpdateAction action) throws IOException { + log.info("编辑操作: {}, {}", entity.getPath(), action); + switch (action) { + case EDIT -> edit(entity); + case CONFIRM -> confirm(entity); + case FALLBACK -> fallback(entity); + } + + } + + private void fallback(EditEntity entity) throws IOException { + String path = entity.getPath(); + String tempPath = getTempPath(path); + Files.move(Paths.get(tempPath), Paths.get(path), StandardCopyOption.REPLACE_EXISTING); + } + + private void confirm(EditEntity entity) throws IOException { + String tempPath = getTempPath(entity.getPath()); + Files.deleteIfExists(Paths.get(tempPath)); + } + + private void edit(EditEntity entity) throws IOException { + String path = entity.getPath(); + String tempPath = getTempPath(path); + Path p = Paths.get(path); + + Snippet snippet = new Snippet(); + BeanUtil.copyProperties(entity, snippet); + + Files.move(p, Paths.get(tempPath), StandardCopyOption.REPLACE_EXISTING); + Files.writeString(p, snippet.toMarkdown(), StandardCharsets.UTF_8); + } + + private String getTempPath(String path) { + return path + ".tmp"; + } + + public void delete(String path) throws IOException { + Files.deleteIfExists(Paths.get(path)); + } + + public String sha(String filePath) { + File file = new File(filePath); + if (!file.exists()) { + throw new RuntimeException("文件不存在: " + filePath); + } + return DigestUtil.sha256Hex(file); + } + + public List listAll() throws IOException { + log.info("获取数据目录[{}]文件信息", System.getenv(Constant.Property.DIR)); + List list = new ArrayList<>(); + File file = new File(System.getenv(Constant.Property.DIR)); + listAll(file, list); + return list; + } + + private void listAll(File file, List list) throws IOException { + File[] files = file.listFiles(); + if (files == null) { + return; + } + for (File f : files) { + if (f.isDirectory()) { + listAll(f, list); + } else { + FileReader reader = new FileReader(f, StandardCharsets.UTF_8); + String s = reader.readAllAsString(); + Snippet snippet = snippetReader.visit(s); + RebuildEntity rebuildEntity = new RebuildEntity(); + BeanUtil.copyProperties(snippet, rebuildEntity); + rebuildEntity.setPath(f.getAbsolutePath()); + rebuildEntity.setSha(DigestUtil.sha256Hex(f)); + rebuildEntity.setName(FileNameUtil.mainName(f)); + list.add(rebuildEntity); + reader.close(); + } + } + } + + public enum UpdateAction { + EDIT, CONFIRM, FALLBACK + } +} diff --git a/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/SnippetReader.java b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/SnippetReader.java new file mode 100644 index 0000000..84c99c2 --- /dev/null +++ b/CodeSnippetDaemon/src/main/java/work/slhaf/snippet/service/SnippetReader.java @@ -0,0 +1,134 @@ +package work.slhaf.snippet.service; + +import org.commonmark.node.*; +import org.commonmark.parser.Parser; +import work.slhaf.snippet.entity.Snippet; + +public class SnippetReader { + + public Snippet visit(String text){ + Snippet snippet = new Snippet(); + Node document = Parser.builder().build().parse(text); + document.accept(new SnippetVisitor(snippet)); + return snippet; + } + + private static class SnippetVisitor extends AbstractVisitor { + + private final Snippet snippet; + private String currentSection = ""; + private String lastMetadataKey = ""; + + public SnippetVisitor(Snippet snippet) { + this.snippet = snippet; + } + + private String extractText(Node node) { + StringBuilder text = new StringBuilder(); + Node child = node.getFirstChild(); + while (child != null) { + if (child instanceof Text) { + text.append(((Text) child).getLiteral()); + } else if (child instanceof Paragraph) { + text.append(extractText(child)); + } + child = child.getNext(); + } + return text.toString(); + } + + @Override + public void visit(FencedCodeBlock fencedCodeBlock) { + // Extract the code content from the fenced code block + String codeContent = fencedCodeBlock.getLiteral(); + if (codeContent != null) { + snippet.setContent(codeContent); + } + + // Extract the language from the info string + String info = fencedCodeBlock.getInfo(); + if (info != null && !info.isEmpty()) { + snippet.setLanguage(info); + } + + super.visit(fencedCodeBlock); + } + + @Override + public void visit(Heading heading) { + // Extract text content from the heading to identify the section + StringBuilder headingText = new StringBuilder(); + Node child = heading.getFirstChild(); + while (child != null) { + if (child instanceof Text) { + headingText.append(((Text) child).getLiteral()); + } + child = child.getNext(); + } + + // Set the current section based on the heading text + currentSection = headingText.toString(); + + super.visit(heading); + } + + private void addTag(String tag) { + String[] existingTags = snippet.getTags(); + if (existingTags == null) { + snippet.setTags(new String[]{tag}); + } else { + String[] newTags = new String[existingTags.length + 1]; + System.arraycopy(existingTags, 0, newTags, 0, existingTags.length); + newTags[existingTags.length] = tag; + snippet.setTags(newTags); + } + } + + @Override + public void visit(BulletList bulletList) { + super.visit(bulletList); + } + + @Override + public void visit(ListItem listItem) { + // Extract text content from the list item + String itemText = extractText(listItem); + + // Remove leading/trailing whitespace + itemText = itemText.trim(); + + // Process metadata based on the current section and item text + if ("MetaData".equals(currentSection)) { + if (itemText.equals("Language")) { + lastMetadataKey = "Language"; + } else if (itemText.equals("Tags")) { + lastMetadataKey = "Tags"; + } else if (itemText.equals("Description")) { + lastMetadataKey = "description"; + } else { + // This is a value for the previous metadata key + if ("Language".equals(lastMetadataKey)) { + snippet.setLanguage(itemText); + } else if ("description".equals(lastMetadataKey)) { + snippet.setDescription(itemText); + } else if ("Tags".equals(lastMetadataKey)) { + // Collect tags + addTag(itemText); + } + } + } + + super.visit(listItem); + } + + @Override + public void visit(Paragraph paragraph) { + super.visit(paragraph); + } + + @Override + public void visit(Text text) { + super.visit(text); + } + } +} diff --git a/CodeSnippetDaemon/src/test/java/MarkdownTest.java b/CodeSnippetDaemon/src/test/java/MarkdownTest.java new file mode 100644 index 0000000..8eee038 --- /dev/null +++ b/CodeSnippetDaemon/src/test/java/MarkdownTest.java @@ -0,0 +1,11 @@ +import work.slhaf.snippet.entity.Snippet; +import work.slhaf.snippet.service.SnippetReader; + +void main() throws IOException { + // Read the test file + FileReader reader = new FileReader("../test/test.md", StandardCharsets.UTF_8); + String s = reader.readAllAsString(); + SnippetReader snippetReader = new SnippetReader(); + Snippet visit = snippetReader.visit(s); + System.out.println(visit); +} \ No newline at end of file diff --git a/CodeSnippetDaemon/src/test/java/SnippetReaderTest.java b/CodeSnippetDaemon/src/test/java/SnippetReaderTest.java new file mode 100644 index 0000000..fa4fbaf --- /dev/null +++ b/CodeSnippetDaemon/src/test/java/SnippetReaderTest.java @@ -0,0 +1,29 @@ +import work.slhaf.snippet.entity.Snippet; +import work.slhaf.snippet.service.SnippetReader; + + void main() throws IOException { + // Read the test file + FileReader reader = new FileReader("../test/test.md", StandardCharsets.UTF_8); + SnippetReader snippetReader = new SnippetReader(); + StringBuilder content = new StringBuilder(); + int ch; + while ((ch = reader.read()) != -1) { + content.append((char) ch); + } + reader.close(); + + // Parse the content using SnippetReader + Snippet snippet = snippetReader.visit(content.toString()); + + // Print the extracted information + System.out.println("Language: " + snippet.getLanguage()); + System.out.println("Description: " + snippet.getDescription()); + System.out.println("Tags: "); + if (snippet.getTags() != null) { + for (String tag : snippet.getTags()) { + System.out.println(" - " + tag); + } + } + System.out.println("Content: "); + System.out.println(snippet.getContent()); +} \ No newline at end of file diff --git a/CodeSnippetRofi/common/constant.py b/CodeSnippetRofi/common/constant.py new file mode 100644 index 0000000..ba86237 --- /dev/null +++ b/CodeSnippetRofi/common/constant.py @@ -0,0 +1,40 @@ +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") + +action_list = "LIST" +action_add = "ADD" +action_edit = "EDIT" +action_delete = "DELETE" + +template_add = """ +## Snippet + +```[Language] + +``` + +## MetaData + +- Name + - +- Language + - +""" +template_edit = """ +## Snippet + +```[Language] + +``` + +## MetaData + +- Tags + - + - +- Description + - +""" \ No newline at end of file diff --git a/CodeSnippetRofi/common/rofi.py b/CodeSnippetRofi/common/rofi.py new file mode 100644 index 0000000..3ef9137 --- /dev/null +++ b/CodeSnippetRofi/common/rofi.py @@ -0,0 +1,823 @@ +# +# python-rofi +# +# The MIT License +# +# Copyright (c) 2016, 2017 Blair Bonnett +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +# + +import atexit +from datetime import datetime +from decimal import Decimal, InvalidOperation +import signal +import subprocess +import time + +# Python < 3.2 doesn't provide a context manager interface for Popen. +# Let's make our own wrapper if needed. +if hasattr(subprocess.Popen, '__exit__'): + Popen = subprocess.Popen +else: + class ContextManagedPopen(subprocess.Popen): + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + if self.stdout: + self.stdout.close() + if self.stderr: + self.stderr.close() + if self.stdin: + self.stdin.close() + self.wait() + + + Popen = ContextManagedPopen + + +class Rofi(object): + """Class to facilitate making simple GUIs with Rofi. + + Rofi is a popup window system with minimal dependencies (xlib and pango). + It was designed as a window switcher. Its basic operation is to display a + list of options and let the user pick one. + + This class provides a set of methods to make simple GUIs with Rofi. It does + this by using the subprocess module to call Rofi externally. Many of the + methods are blocking. + + Some strings can contain Pango markup for additional formatting (those that + can are noted as such in the docstrings). Any text in these strings *must* + be escaped before calling Rofi. The class method Rofi.escape() performs + this escaping for you. Make sure you call this on the text prior to adding + Pango markup, otherwise the markup will be escaped and displayed to the + user. See https://developer.gnome.org/pango/stable/PangoMarkupFormat.html + for available markup. + + """ + + def __init__(self, lines=None, fixed_lines=None, width=None, + fullscreen=None, location=None, + exit_hotkeys=('Alt+F4', 'Control+q'), rofi_cmd=None): + """ + Parameters + ---------- + exit_hotkeys: tuple of strings + Hotkeys to use to exit the application. These will be automatically + set and handled in any method which takes hotkey arguments. If one + of these hotkeys is pressed, a SystemExit will be raised to perform + the exit. + + The following parameters set default values for various layout options, + and can be overwritten in any display method. A value of None means + use the system default, which may be set by a configuration file or + fall back to the compile-time default. See the Rofi documentation for + full details on what the values mean. + + lines: positive integer + The maximum number of lines to show before scrolling. + fixed_lines: positive integer + Keep a fixed number of lines visible. + width: real + If positive but not more than 100, this is the percentage of the + screen's width the window takes up. If greater than 100, it is the + width in pixels. If negative, it estimates the width required for + the corresponding number of characters, i.e., -30 would set the + width so ~30 characters per row would show. + fullscreen: boolean + If True, use the full height and width of the screen. + location: integer + The position of the window on the screen. + + """ + # The Popen class returned for any non-blocking windows. + self._process = None + + # Save parameters. + self.lines = lines + self.fixed_lines = fixed_lines + self.width = width + self.fullscreen = fullscreen + self.location = location + self.exit_hotkeys = exit_hotkeys + + if rofi_cmd is None: + self.rofi = "rofi" + else: + self.rofi = rofi_cmd + # Don't want a window left on the screen if we exit unexpectedly + # (e.g., an unhandled exception). + atexit.register(self.close) + + @classmethod + def escape(self, string): + """Escape a string for Pango markup. + + Parameters + ---------- + string: + A piece of text to escape. + + Returns + ------- + The text, safe for use in with Pango markup. + + """ + # Escape ampersands first, then other entities. Since argument is a + # dictionary, we can't guarantee order of translations and so doing it + # in one go would risk the ampersands in other translations being + # escaped again. + return string.translate( + {38: '&'} + ).translate({ + 34: '"', + 39: ''', + 60: '<', + 62: '>' + }) + + def close(self): + """Close any open window. + + Note that this only works with non-blocking methods. + + """ + if self._process: + # Be nice first. + self._process.send_signal(signal.SIGINT) + + # If it doesn't close itself promptly, be brutal. + # Python 3.2+ added the timeout option to wait() and the + # corresponding TimeoutExpired exception. If they exist, use them. + if hasattr(subprocess, 'TimeoutExpired'): + try: + self._process.wait(timeout=1) + except subprocess.TimeoutExpired: + self._process.send_signal(signal.SIGKILL) + + # Otherwise, roll our own polling loop. + else: + # Give it 1s, checking every 10ms. + count = 0 + while count < 100: + if self._process.poll() is not None: + break + time.sleep(0.01) + + # Still hasn't quit. + if self._process.poll() is None: + self._process.send_signal(signal.SIGKILL) + + # Clean up. + self._process = None + + def _run_blocking(self, args, input=None): + """Internal API: run a blocking command with subprocess. + + This closes any open non-blocking dialog before running the command. + + Parameters + ---------- + args: Popen constructor arguments + Command to run. + input: string + Value to feed to the stdin of the process. + + Returns + ------- + (returncode, stdout) + The exit code (integer) and stdout value (string) from the process. + + """ + # Close any existing dialog. + if self._process: + self.close() + + # Make sure we grab stdout as text (not bytes). + kwargs = {} + kwargs['stdout'] = subprocess.PIPE + kwargs['universal_newlines'] = True + + # Use the run() method if available (Python 3.5+). + if hasattr(subprocess, 'run'): + result = subprocess.run(args, input=input, **kwargs) + return result.returncode, result.stdout + + # Have to do our own. If we need to feed stdin, we must open a pipe. + if input is not None: + kwargs['stdin'] = subprocess.PIPE + + # Start the process. + with Popen(args, **kwargs) as proc: + # Talk to it (no timeout). This will wait until termination. + stdout, stderr = proc.communicate(input) + + # Find out the return code. + returncode = proc.poll() + + # Done. + return returncode, stdout + + def _run_nonblocking(self, args, input=None): + """Internal API: run a non-blocking command with subprocess. + + This closes any open non-blocking dialog before running the command. + + Parameters + ---------- + args: Popen constructor arguments + Command to run. + input: string + Value to feed to the stdin of the process. + + """ + # Close any existing dialog. + if self._process: + self.close() + + # Start the new one. + self._process = subprocess.Popen(args, stdout=subprocess.PIPE) + + def _common_args(self, allow_fullscreen=True, **kwargs): + args = [] + + # Number of lines. + lines = kwargs.get('lines', self.lines) + if lines: + args.extend(['-lines', str(lines)]) + fixed_lines = kwargs.get('fixed_lines', self.fixed_lines) + if fixed_lines: + args.extend(['-fixed-num-lines', str(fixed_lines)]) + + # Width. + width = kwargs.get('width', self.width) + if width is not None: + args.extend(['-width', str(width)]) + + # Fullscreen mode? + fullscreen = kwargs.get('fullscreen', self.fullscreen) + if allow_fullscreen and fullscreen: + args.append('-fullscreen') + + # Location on screen. + location = kwargs.get('location', self.location) + if location is not None: + args.extend(['-location', str(location)]) + + # Done. + return args + + def error(self, message, **kwargs): + """Show an error window. + + This method blocks until the user presses a key. + + Fullscreen mode is not supported for error windows, and if specified + will be ignored. + + Parameters + ---------- + message: string + Error message to show. + + """ + # Generate arguments list. + args = [self.rofi, '-e', message] + args.extend(self._common_args(allow_fullscreen=False, **kwargs)) + + # Close any existing window and show the error. + self._run_blocking(args) + + def status(self, message, **kwargs): + """Show a status message. + + This method is non-blocking, and intended to give a status update to + the user while something is happening in the background. + + To close the window, either call the close() method or use any of the + display methods to replace it with a different window. + + Fullscreen mode is not supported for status messages and if specified + will be ignored. + + Parameters + ---------- + message: string + Progress message to show. + + """ + # Generate arguments list. + args = [self.rofi, '-e', message] + args.extend(self._common_args(allow_fullscreen=False, **kwargs)) + + # Update the status. + self._run_nonblocking(args) + + def select(self, prompt, options, message="", select=None, **kwargs): + """Show a list of options and return user selection. + + This method blocks until the user makes their choice. + + Parameters + ---------- + prompt: string + The prompt telling the user what they are selecting. + options: list of strings + The options they can choose from. Any newline characters are + replaced with spaces. + message: string, optional + Message to show between the prompt and the options. This can + contain Pango markup, and any text content should be escaped. + select: integer, optional + Set which option is initially selected. + keyN: tuple (string, string); optional + Custom key bindings where N is one or greater. The first entry in + the tuple should be a string defining the key, e.g., "Alt+x" or + "Delete". Note that letter keys should be lowercase ie.e., Alt+a + not Alt+A. + + The second entry should be a short string stating the action the + key will take. This is displayed to the user at the top of the + dialog. If None or an empty string, it is not displayed (but the + binding is still set). + + By default, key1 through key9 are set to ("Alt+1", None) through + ("Alt+9", None) respectively. + + Returns + ------- + tuple (index, key) + The index of the option the user selected, or -1 if they cancelled + the dialog. + Key indicates which key was pressed, with 0 being 'OK' (generally + Enter), -1 being 'Cancel' (generally escape), and N being custom + key N. + + """ + # Replace newlines and turn the options into a single string. + optionstr = '\n'.join(option.replace('\n', ' ') for option in options) + + # Set up arguments. + args = [self.rofi, '-dmenu', '-p', prompt, '-format', 'i'] + if select is not None: + args.extend(['-selected-row', str(select)]) + + # Key bindings to display. + display_bindings = [] + + # Configure the key bindings. + user_keys = set() + for k, v in kwargs.items(): + # See if the keyword name matches the needed format. + if not k.startswith('key'): + continue + try: + keynum = int(k[3:]) + except ValueError: + continue + + # Add it to the set. + key, action = v + user_keys.add(keynum) + args.extend(['-kb-custom-{0:s}'.format(k[3:]), key]) + if action: + display_bindings.append("{0:s}: {1:s}".format(key, action)) + + # And the global exit bindings. + exit_keys = set() + next_key = 10 + for key in self.exit_hotkeys: + while next_key in user_keys: + next_key += 1 + exit_keys.add(next_key) + args.extend(['-kb-custom-{0:d}'.format(next_key), key]) + next_key += 1 + + # Add any displayed key bindings to the message. + message = message or "" + if display_bindings: + message += "\n" + " ".join(display_bindings) + message = message.strip() + + # If we have a message, add it to the arguments. + if message: + args.extend(['-mesg', message]) + + # Add in common arguments. + args.extend(self._common_args(**kwargs)) + + # Run the dialog. + returncode, stdout = self._run_blocking(args, input=optionstr) + + # Figure out which option was selected. + stdout = stdout.strip() + index = int(stdout) if stdout else -1 + + # And map the return code to a key. + if returncode == 0: + key = 0 + elif returncode == 1: + key = -1 + elif returncode > 9: + key = returncode - 9 + if key in exit_keys: + raise SystemExit() + else: + self.exit_with_error("Unexpected rofi returncode {0:d}.".format(results.returncode)) + + # And return. + return index, key + + def generic_entry(self, prompt, validator=None, message=None, **kwargs): + """A generic entry box. + + Parameters + ---------- + prompt: string + Text prompt for the entry. + validator: function, optional + A function to validate and convert the value entered by the user. + It should take one parameter, the string that the user entered, and + return a tuple (value, error). The value should be the users entry + converted to the appropriate Python type, or None if the entry was + invalid. The error message should be a string telling the user what + was wrong, or None if the entry was valid. The prompt will be + re-displayed to the user (along with the error message) until they + enter a valid value. If no validator is given, the text that the + user entered is returned as-is. + message: string + Optional message to display under the entry. + + Returns + ------- + The value returned by the validator, or None if the dialog was + cancelled. + + Examples + -------- + Enforce a minimum entry length: + >>> r = Rofi() + >>> validator = lambda s: (s, None) if len(s) > 6 else (None, "Too short") + >>> r.generic_entry('Enter a 7-character or longer string: ', validator) + + """ + error = "" + + # Keep going until we get something valid. + while True: + args = [self.rofi, '-dmenu', '-p', prompt, '-format', 's'] + + # Add any error to the given message. + msg = message or "" + if error: + msg = '{0:s}\n{1:s}'.format(error, msg) + msg = msg.rstrip('\n') + + # If there is actually a message to show. + if msg: + args.extend(['-mesg', msg]) + + # Add in common arguments. + args.extend(self._common_args(**kwargs)) + + # Run it. + returncode, stdout = self._run_blocking(args, input="") + + # Was the dialog cancelled? + if returncode == 1: + return None + + # Get rid of the trailing newline and check its validity. + text = stdout.rstrip('\n') + if validator: + value, error = validator(text) + if not error: + return value + else: + return text + + def text_entry(self, prompt, message=None, allow_blank=False, strip=True, **kwargs): + """Prompt the user to enter a piece of text. + + Parameters + ---------- + prompt: string + Prompt to display to the user. + message: string, optional + Message to display under the entry line. + allow_blank: Boolean + Whether to allow blank entries. + strip: Boolean + Whether to strip leading and trailing whitespace from the entered + value. + + Returns + ------- + string, or None if the dialog was cancelled. + + """ + + def text_validator(text): + if strip: + text = text.strip() + if not allow_blank: + if not text: + return None, "A value is required." + + return text, None + + return self.generic_entry(prompt, text_validator, message, **kwargs) + + def integer_entry(self, prompt, message=None, min=None, max=None, **kwargs): + """Prompt the user to enter an integer. + + Parameters + ---------- + prompt: string + Prompt to display to the user. + message: string, optional + Message to display under the entry line. + min, max: integer, optional + Minimum and maximum values to allow. If None, no limit is imposed. + + Returns + ------- + integer, or None if the dialog is cancelled. + + """ + # Sanity check. + if (min is not None) and (max is not None) and not (max > min): + raise ValueError("Maximum limit has to be more than the minimum limit.") + + def integer_validator(text): + error = None + + # Attempt to convert to integer. + try: + value = int(text) + except ValueError: + return None, "Please enter an integer value." + + # Check its within limits. + if (min is not None) and (value < min): + return None, "The minimum allowable value is {0:d}.".format(min) + if (max is not None) and (value > max): + return None, "The maximum allowable value is {0:d}.".format(max) + + return value, None + + return self.generic_entry(prompt, integer_validator, message, **kwargs) + + def float_entry(self, prompt, message=None, min=None, max=None, **kwargs): + """Prompt the user to enter a floating point number. + + Parameters + ---------- + prompt: string + Prompt to display to the user. + message: string, optional + Message to display under the entry line. + min, max: float, optional + Minimum and maximum values to allow. If None, no limit is imposed. + + Returns + ------- + float, or None if the dialog is cancelled. + + """ + # Sanity check. + if (min is not None) and (max is not None) and not (max > min): + raise ValueError("Maximum limit has to be more than the minimum limit.") + + def float_validator(text): + error = None + + # Attempt to convert to float. + try: + value = float(text) + except ValueError: + return None, "Please enter a floating point value." + + # Check its within limits. + if (min is not None) and (value < min): + return None, "The minimum allowable value is {0}.".format(min) + if (max is not None) and (value > max): + return None, "The maximum allowable value is {0}.".format(max) + + return value, None + + return self.generic_entry(prompt, float_validator, message, **kwargs) + + def decimal_entry(self, prompt, message=None, min=None, max=None, **kwargs): + """Prompt the user to enter a decimal number. + + Parameters + ---------- + prompt: string + Prompt to display to the user. + message: string, optional + Message to display under the entry line. + min, max: Decimal, optional + Minimum and maximum values to allow. If None, no limit is imposed. + + Returns + ------- + Decimal, or None if the dialog is cancelled. + + """ + # Sanity check. + if (min is not None) and (max is not None) and not (max > min): + raise ValueError("Maximum limit has to be more than the minimum limit.") + + def decimal_validator(text): + error = None + + # Attempt to convert to decimal. + try: + value = Decimal(text) + except InvalidOperation: + return None, "Please enter a decimal value." + + # Check its within limits. + if (min is not None) and (value < min): + return None, "The minimum allowable value is {0}.".format(min) + if (max is not None) and (value > max): + return None, "The maximum allowable value is {0}.".format(max) + + return value, None + + return self.generic_entry(prompt, decimal_validator, message, **kwargs) + + def date_entry(self, prompt, message=None, formats=['%x', '%d/%m/%Y'], show_example=False, **kwargs): + """Prompt the user to enter a date. + + Parameters + ---------- + prompt: string + Prompt to display to the user. + message: string, optional + Message to display under the entry line. + formats: list of strings, optional + The formats that the user can enter dates in. These should be + format strings as accepted by the datetime.datetime.strptime() + function from the standard library. They are tried in order, and + the first that returns a date object without error is selected. + Note that the '%x' in the default list is the current locale's date + representation. + show_example: Boolean + If True, today's date in the first format given is appended to the + message. + + Returns + ------- + datetime.date, or None if the dialog is cancelled. + + """ + + def date_validator(text): + # Try them in order. + for format in formats: + try: + dt = datetime.strptime(text, format) + except ValueError: + continue + else: + # This one worked; good enough for us. + return (dt.date(), None) + + # None of the formats worked. + return (None, 'Please enter a valid date.') + + # Add an example to the message? + if show_example: + message = message or "" + message += "Today's date in the correct format: " + datetime.now().strftime(formats[0]) + + return self.generic_entry(prompt, date_validator, message, **kwargs) + + def time_entry(self, prompt, message=None, formats=['%X', '%H:%M', '%I:%M', '%H.%M', '%I.%M'], show_example=False, + **kwargs): + """Prompt the user to enter a time. + + Parameters + ---------- + prompt: string + Prompt to display to the user. + message: string, optional + Message to display under the entry line. + formats: list of strings, optional + The formats that the user can enter times in. These should be + format strings as accepted by the datetime.datetime.strptime() + function from the standard library. They are tried in order, and + the first that returns a time object without error is selected. + Note that the '%X' in the default list is the current locale's time + representation. + show_example: Boolean + If True, the current time in the first format given is appended to + the message. + + Returns + ------- + datetime.time, or None if the dialog is cancelled. + + """ + + def time_validator(text): + # Try them in order. + for format in formats: + try: + dt = datetime.strptime(text, format) + except ValueError: + continue + else: + # This one worked; good enough for us. + return (dt.time(), None) + + # None of the formats worked. + return (None, 'Please enter a valid time.') + + # Add an example to the message? + if show_example: + message = message or "" + message += "Current time in the correct format: " + datetime.now().strftime(formats[0]) + + return self.generic_entry(prompt, time_validator, message, **kwargs) + + def datetime_entry(self, prompt, message=None, formats=['%x %X'], show_example=False, **kwargs): + """Prompt the user to enter a date and time. + + Parameters + ---------- + prompt: string + Prompt to display to the user. + message: string, optional + Message to display under the entry line. + formats: list of strings, optional + The formats that the user can enter the date and time in. These + should be format strings as accepted by the + datetime.datetime.strptime() function from the standard library. + They are tried in order, and the first that returns a datetime + object without error is selected. Note that the '%x %X' in the + default list is the current locale's date and time representation. + show_example: Boolean + If True, the current date and time in the first format given is appended to + the message. + + Returns + ------- + datetime.datetime, or None if the dialog is cancelled. + + """ + + def datetime_validator(text): + # Try them in order. + for format in formats: + try: + dt = datetime.strptime(text, format) + except ValueError: + continue + else: + # This one worked; good enough for us. + return (dt, None) + + # None of the formats worked. + return (None, 'Please enter a valid date and time.') + + # Add an example to the message? + if show_example: + message = message or "" + message += "Current date and time in the correct format: " + datetime.now().strftime(formats[0]) + + return self.generic_entry(prompt, datetime_validator, message, **kwargs) + + def exit_with_error(self, error, **kwargs): + """Report an error and exit. + + This raises a SystemExit exception to ask the interpreter to quit. + + Parameters + ---------- + error: string + The error to report before quitting. + + """ + self.error(error, **kwargs) + raise SystemExit(error) diff --git a/CodeSnippetRofi/entity/response.py b/CodeSnippetRofi/entity/response.py new file mode 100644 index 0000000..6431bcc --- /dev/null +++ b/CodeSnippetRofi/entity/response.py @@ -0,0 +1,19 @@ +import json + + +class SearchResponse: + def __init__(self, response: dict): + self.status = response["status"] + self.data = [SearchResponse.SearchData(dict(r)) for r in json.loads(response["data"])] + + class SearchData: + def __init__(self, data: dict): + self.id = data["id"] + self.name = data["name"] + self.path = data["path"] + self.score = data["score"] + +class NormalResponse: + def __init__(self, response: dict): + self.status = response["status"] + self.data = response["data"] diff --git a/CodeSnippetRofi/entity/result.py b/CodeSnippetRofi/entity/result.py new file mode 100644 index 0000000..74eaf25 --- /dev/null +++ b/CodeSnippetRofi/entity/result.py @@ -0,0 +1,4 @@ +class ActionResult: + def __init__(self, ok: bool, message: str): + self.ok = ok + self.message = message \ No newline at end of file diff --git a/CodeSnippetRofi/helper/api_helper.py b/CodeSnippetRofi/helper/api_helper.py new file mode 100644 index 0000000..b92270e --- /dev/null +++ b/CodeSnippetRofi/helper/api_helper.py @@ -0,0 +1,58 @@ +import json +import subprocess + +from common.constant import code_snippet_port, action_list, action_edit, action_delete, action_add +from entity.response import SearchResponse, NormalResponse + + +def _run_api(action: str, data: str) -> dict: + action = action.upper() + if action not in ["LIST", "ADD", "EDIT", "DELETE"]: + raise ValueError("Invalid action") + + message = { + "action": f"{action}", + "data": f"{data}" + } + + # 使用 Popen 创建管道 + echo_process = subprocess.Popen( + ["echo", json.dumps(message)], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + nc_process = subprocess.Popen( + ["nc", "-q", "0", "127.0.0.1", code_snippet_port], + stdin=echo_process.stdout, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True + ) + + # 关闭 echo 的输出,避免资源泄漏 + echo_process.stdout.close() + + try: + # 获取 nc 的输出 + nc_stdout, nc_stderr = nc_process.communicate() + return dict(json.loads(nc_stdout)) + except Exception: + raise ConnectionError(f"后端交互失败!检查守护进程是否运行") + + +def run_search(key: str) -> SearchResponse: + return SearchResponse(_run_api(action_list, key)) + + +def run_edit(data: str) -> NormalResponse: + return NormalResponse(_run_api(action_edit, data)) + + +def run_delete(path: str) -> NormalResponse: + return NormalResponse(_run_api(action_delete, path)) + + +def run_add(data: str) -> NormalResponse: + return NormalResponse(_run_api(action_add, data)) diff --git a/CodeSnippetRofi/helper/file_helper.py b/CodeSnippetRofi/helper/file_helper.py new file mode 100644 index 0000000..0044c46 --- /dev/null +++ b/CodeSnippetRofi/helper/file_helper.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python3 +import hashlib +import json +import os +import re +import shutil +import subprocess +import tempfile +from pathlib import Path + +from common.constant import template_add, template_edit +from entity.result import ActionResult +from helper.api_helper import run_edit, run_add + + +def _parse_add_text(text: str) -> str: + # 提取第一个代码块(包含可选的 ```[Lang] 或 ```Lang) + fence_re = re.search(r"```(?:\[(?P[^\]]*)\]|(?P[A-Za-z0-9_+\-]+))?\s*\r?\n(?P[\s\S]*?)```", text) + fence_lang = '' + content = '' + if fence_re: + fence_lang = (fence_re.group('fence_lang') or fence_re.group('fence_lang2') or '') or '' + content = (fence_re.group('code') or '').strip() + # 把模板占位符当做空值处理 + if fence_lang.strip().lower() == 'language' or fence_lang.strip() == '': + fence_lang = '' + else: + fence_lang = fence_lang.strip() + + # 定位 MetaData 区块(从"## MetaData"到文档末尾或下一个"## ") + meta_match = re.search(r"##\s*MetaData\s*(?P[\s\S]*)", text, flags=re.I) + meta = meta_match.group('meta') if meta_match else '' + meta_lines = meta.splitlines() + + def extract_list_value(field_name: str) -> str: + field_name_re = re.compile(rf"^\s*-\s*{re.escape(field_name)}\s*$", flags=re.I) + list_item_re = re.compile(r"^\s*-\s*(.*)$") + for i, line in enumerate(meta_lines): + if field_name_re.match(line): + # 找下一条非空行作为子项 + j = i + 1 + while j < len(meta_lines) and meta_lines[j].strip() == "": + j += 1 + if j < len(meta_lines): + m = list_item_re.match(meta_lines[j]) + if m: + val = m.group(1).strip() + # 把模板占位符当做空值处理 + if val.lower() == field_name.lower() or val.lower() == 'language' and field_name.lower() == 'language' and val == '': + return '' + return val + return "" + return "" + + name = extract_list_value("Name") + language_meta = extract_list_value("Language") + + # 优先使用 MetaData 下方的 Language,若无再使用代码块 fence 的语言 + language = language_meta or fence_lang or "" + + entity = { + "name": name or "", + "language": language or "", + "content": content or "", + } + return json.dumps(entity, ensure_ascii=False) + +# 包装成读取文件的版本(保留原函数签名) +def _parse_add(file_path: str) -> str: + text = Path(file_path).read_text(encoding='utf-8') + return _parse_add_text(text) + + +def _prefill_edit(file_path: str, source_path: str, _id: str) -> None: + # 将原始文件内容与元信息写入模板占位 + # 读取原文件 + src = Path(source_path).read_text(encoding='utf-8') + + # 提取代码块(仅代码,不带多余模板)与语言 + code_block = re.search(r"```(?:\[([^\]]*)\]|([A-Za-z0-9_+\-]+))?\s*\r?\n([\s\S]*?)```", src) + language = '' + code_only = '' + if code_block: + language = (code_block.group(1) or code_block.group(2) or '').strip() + code_only = (code_block.group(3) or '').strip() + # 若未检测到代码块则回退为全文 + if not code_only: + code_only = src.strip() + + # 从源文档的 MetaData 段提取 tags 与 description + tags: list[str] = [] + desc = '' + # 定位 MetaData 段 + meta_match = re.search(r"##\s*MetaData([\s\S]*)", src) + meta = meta_match.group(1) if meta_match else '' + if meta: + # Tags 段 + tags_section = re.search(r"-\s*Tags\s*([\s\S]*?)(?:\n-\s*Description|\Z)", meta) + if tags_section: + for line in tags_section.group(1).splitlines(): + m = re.match(r"^\s*-\s*(.+)$", line) + if m: + val = m.group(1).strip() + if val: + tags.append(val) + # Description 段 + desc_section = re.search(r"-\s*Description\s*\n\s*-\s*(.+)", meta) + if desc_section: + desc = desc_section.group(1).strip() + + # 若语言未能从代码块检测,则用路径上级名兜底 + if not language: + language = Path(source_path).parent.name + + # 重建模板(覆盖写入) + rebuilt_lines = [] + rebuilt_lines.append("## Snippet\n\n") + if language: + rebuilt_lines.append(f"```{language}\n") + else: + rebuilt_lines.append("```\n") + rebuilt_lines.append(code_only) + rebuilt_lines.append("\n```\n\n") + rebuilt_lines.append("## MetaData\n\n") + rebuilt_lines.append("- Tags\n") + if tags: + for t in tags: + rebuilt_lines.append(f" - {t}\n") + else: + rebuilt_lines.append(" - \n - \n") + rebuilt_lines.append("- Description\n") + rebuilt_lines.append(f" - {desc}\n") + + Path(file_path).write_text(''.join(rebuilt_lines), encoding='utf-8') + + +def _parse_edit(file_path: str, _id: str, source_path: str) -> str: + text = Path(file_path).read_text(encoding='utf-8') + # 解析语言(不强制需要) + # 解析代码块内容 + code_match = re.search(r"```(?:\[[^\]]*\]|[A-Za-z0-9_+\-]*)\s*\r?\n([\s\S]*?)```", text) + content = (code_match.group(1) if code_match else '').strip() + # tags + tag_lines = re.findall(r"^-\s+(.*)$", text, flags=re.M) + # 在 '- Tags' 之后的两级缩进项 + tags_section = False + tags = [] + for line in text.splitlines(): + if re.match(r"^-\s*Tags\s*$", line): + tags_section = True + continue + if tags_section: + m = re.match(r"^\s*-\s*(.*)$", line) + if m: + val = m.group(1).strip() + if val: + tags.append(val) + else: + # 离开 tags 段 + tags_section = False + # 到 Description 再退出 + if re.match(r"^-\s*Description\s*$", line): + tags_section = False + + desc_match = re.search(r"-\s*Description\s*\n\s*-\s*(.*)", text) + description = (desc_match.group(1) if desc_match else '').strip() + + entity = { + "id": _id, + "path": source_path, + "tags": tags, + "description": description, + "content": content, + } + return json.dumps(entity, ensure_ascii=False) + + +def _create_tmp(content: str) -> str: + # 创建临时文件,并写入template_path对应的文件中的内容,最终返回临时文件的路径 + # 临时文件路径固定为/tmp/<随机字符串>.md + with tempfile.NamedTemporaryFile(prefix='', suffix='.md', dir='/tmp/', delete=False) as tmp_file: + temp_path = tmp_file.name + + # 将模板文件内容复制到临时文件 + Path(temp_path).write_text(content, encoding='utf-8') + + return temp_path + + +def _open_with_nvim(file_path: str) -> None: + # 获取终端环境变量,默认为xterm + term = os.environ.get('TERMINAL', '') + if not term: + term = 'xterm' + + # 获取终端名称 + name = os.path.basename(term) + + # 根据不同终端类型构建命令 + cmd = [term] + + # 根据终端类型添加特定参数 + if name == 'alacritty': + cmd.extend(['--class', 'floating-nvim', '-e', 'nvim', file_path]) + elif name == 'kitty': + cmd.extend(['--class', 'floating-nvim', 'nvim', file_path]) + elif name == 'foot': + cmd.extend(['-a', 'floating-nvim', 'nvim', file_path]) + elif name == 'wezterm': + cmd.extend(['start', '--class', 'floating-nvim', '--', 'nvim', file_path]) + elif name.startswith('gnome-terminal'): + cmd.extend(['--class=floating-nvim', '--', 'nvim', file_path]) + elif name == 'konsole': + cmd.extend(['--class', 'floating-nvim', '-e', 'nvim', file_path]) + elif name == 'urxvt': + cmd.extend(['-name', 'floating-nvim', '-e', 'nvim', file_path]) + elif name == 'xterm': + cmd.extend(['-class', 'floating-nvim', '-e', 'nvim', file_path]) + else: + cmd.extend(['-e', 'nvim', file_path]) + + # 阻塞执行命令,直到进程结束 + subprocess.run(cmd) + + +def _copy(tmp_path: str) -> None: + # 读取临时文件内容并提取代码块 + text = Path(tmp_path).read_text(encoding='utf-8') + m = re.search(r"```(?:\[[^\]]*\]|[A-Za-z0-9_+\-]*)\s*\r?\n([\s\S]*?)```", text) + if not m: + code = "" + else: + code = m.group(1).strip() + + # 检查是否有内容 + if not code: + Path(tmp_path).unlink(missing_ok=True) + return + + # 检查是否有 wl-copy 命令 + if not shutil.which('wl-copy'): + print("Error: 未安装 wl-copy") + Path(tmp_path).unlink(missing_ok=True) + return + + # 复制到剪贴板 + process = subprocess.Popen(['wl-copy'], stdin=subprocess.PIPE, text=True) + process.communicate(input=code) + + +def _calculate_file_sha256(file_path): + """ + 计算文件的SHA256哈希值 + + Args: + file_path (str): 文件路径 + + Returns: + str: 文件的SHA256哈希值(十六进制字符串) + """ + # 创建SHA256哈希对象 + sha256_hash = hashlib.sha256() + + # 以二进制模式打开文件 + with open(file_path, 'rb') as file: + # 分块读取文件,避免内存问题 + for byte_block in iter(lambda: file.read(4096), b""): + sha256_hash.update(byte_block) + + # 返回十六进制格式的哈希值 + return sha256_hash.hexdigest() + +def edit_and_copy(path: str, id: str) -> ActionResult: + tmp_path = _create_tmp(template_edit) + try: + _prefill_edit(tmp_path, path, id) + _open_with_nvim(tmp_path) + _copy(tmp_path) + # 清理临时文件 + return ActionResult(True, "") + except Exception as e: + print(f"Error: {e}") + return ActionResult(False, str(e)) + finally: + Path(tmp_path).unlink(missing_ok=True) + +def edit(path: str, id: str) -> ActionResult: + tmp_path = _create_tmp(template_edit) + primary_hash = _calculate_file_sha256(path) + try: + _prefill_edit(tmp_path, path, id) + _open_with_nvim(tmp_path) + new_hash = _calculate_file_sha256(path) + if primary_hash == new_hash: + return ActionResult(True, "文件未修改") + edit_json = _parse_edit(tmp_path, id, path) + response = run_edit(edit_json) + if response.status == "SUCCESS": + return ActionResult(True, response.data) + else: + return ActionResult(False, response.data) + except Exception as e: + print(f"Error: {e}") + return ActionResult(False, str(e)) + finally: + Path(tmp_path).unlink(missing_ok=True) + + +def add() -> ActionResult: + tmp_path = _create_tmp(template_add) + primary_hash = _calculate_file_sha256(tmp_path) + try: + _open_with_nvim(tmp_path) + new_hash = _calculate_file_sha256(tmp_path) + if primary_hash == new_hash: + return ActionResult(True, "内容为空") + add_json = _parse_add(tmp_path) + response = run_add(add_json) + if response.status == "SUCCESS": + return ActionResult(True, response.data) + else: + return ActionResult(False, response.data) + except Exception as e: + print(f"Error: {e}") + return ActionResult(False, str(e)) + finally: + Path(tmp_path).unlink(missing_ok=True) + + +if __name__ == '__main__': + tmp_path = _create_tmp(template_add) + _open_with_nvim(tmp_path) + add_json = _parse_add(tmp_path) + print(add_json) diff --git a/CodeSnippetRofi/launcher.py b/CodeSnippetRofi/launcher.py new file mode 100755 index 0000000..a4d314a --- /dev/null +++ b/CodeSnippetRofi/launcher.py @@ -0,0 +1,11 @@ +import os + +from common.constant import code_snippet_rofi +from common import rofi +from menu.MainMenu import MainMenu + +if code_snippet_rofi is None: + r = rofi.Rofi() +else: + r = rofi.Rofi(code_snippet_rofi) +MainMenu(r).run() diff --git a/CodeSnippetRofi/menu/AddMenu.py b/CodeSnippetRofi/menu/AddMenu.py new file mode 100644 index 0000000..4d7bcdb --- /dev/null +++ b/CodeSnippetRofi/menu/AddMenu.py @@ -0,0 +1,13 @@ +import rofi + +from helper.file_helper import add + + +class AddMenu: + def __init__(self,r:rofi.Rofi): + self._r = r + + def run(self): + result = add() + if not result.ok: + self._r.error(result.message) diff --git a/CodeSnippetRofi/menu/DeleteMenu.py b/CodeSnippetRofi/menu/DeleteMenu.py new file mode 100644 index 0000000..3b4e585 --- /dev/null +++ b/CodeSnippetRofi/menu/DeleteMenu.py @@ -0,0 +1,44 @@ +import rofi + +from helper.api_helper import run_search, run_delete + + +class DeleteMenu: + def __init__(self,r: rofi.Rofi): + self._r = r + + def run(self): + try: + result = run_search("") + except Exception as e: + print(f"Error: {e}") + self._r.error(str(e)) + return + if result.status != "SUCCESS": + self._r.error(result.data) + return + while True: + if len(result.data) == 0: + self._r.select("删除", ["未找到Snippet记录"]) + break + options = [f"{d.name} - {d.path}" for d in result.data] + index, key = self._r.select("删除", options) + if key == -1: + break + ConfirmMenu(self._r,result.data[index].path).run() + + +class ConfirmMenu: + def __init__(self,r:rofi.Rofi, path: str): + self._r = r + self.path = path + + def run(self): + options = ["是", "否"] + index, key = self._r.select("确定删除?", options) + if key == -1: + return + if index == 0: + res = run_delete(self.path) + if res.status != "SUCCESS": + self._r.error(res.data) diff --git a/CodeSnippetRofi/menu/EditMenu.py b/CodeSnippetRofi/menu/EditMenu.py new file mode 100644 index 0000000..c1ae001 --- /dev/null +++ b/CodeSnippetRofi/menu/EditMenu.py @@ -0,0 +1,32 @@ +import rofi + +from helper.api_helper import run_search +from helper.file_helper import edit + + +class EditMenu: + def __init__(self,r: rofi.Rofi): + self._r = r + + def run(self): + try: + result = run_search("") + except Exception as e: + print(f"Error: {e}") + self._r.error(str(e)) + return + if result.status != "SUCCESS": + self._r.error(result.data) + return + while True: + if len(result.data) == 0: + self._r.select("编辑", ["未找到Snippet记录"]) + break + options = [f"{d.name} - {d.path}" for d in result.data] + index, key = self._r.select("编辑", options) + if key == -1: + break + data = result.data[index] + res = edit(data.path, data.id) + if not res.ok: + self._r.error(res.message) diff --git a/CodeSnippetRofi/menu/MainMenu.py b/CodeSnippetRofi/menu/MainMenu.py new file mode 100644 index 0000000..9a6b1fd --- /dev/null +++ b/CodeSnippetRofi/menu/MainMenu.py @@ -0,0 +1,29 @@ +import rofi + +from menu.AddMenu import AddMenu +from menu.DeleteMenu import DeleteMenu +from menu.EditMenu import EditMenu +from menu.SearchMenu import SearchMenu + + +class MainMenu: + def __init__(self,r: rofi.Rofi): + self._r = r + + def run(self): + while True: + options = ["搜索", "编辑", "添加", "删除"] + index, key = self._r.select("Code Snippet", options) + print(f"index: {index}") + print(f"key: {key}") + if key == -1: + break + match index: + case 0: + SearchMenu(self._r).run() + case 1: + EditMenu(self._r).run() + case 2: + AddMenu(self._r).run() + case 3: + DeleteMenu(self._r).run() diff --git a/CodeSnippetRofi/menu/SearchMenu.py b/CodeSnippetRofi/menu/SearchMenu.py new file mode 100644 index 0000000..ad5d954 --- /dev/null +++ b/CodeSnippetRofi/menu/SearchMenu.py @@ -0,0 +1,71 @@ +import rofi + +from entity.response import SearchResponse +from helper.api_helper import run_search, run_delete +from helper.file_helper import edit, edit_and_copy + + +class SearchMenu: + def __init__(self, r: rofi.Rofi): + self._r = r + + def run(self): + while True: + key = self._r.text_entry("搜索", allow_blank=True) + if key is None: + break + print(key) + try: + res = run_search(key) + except Exception as e: + print(f"Error: {e}") + self._r.error(str(e)) + return + if res.status != "SUCCESS": + self._r.error(res.data) + return + SearchResultMenu(self._r, res.data).run() + + +class SearchResultMenu: + def __init__(self, r: rofi.Rofi, data: list[SearchResponse.SearchData]): + self._r = r + self._data = data + + def run(self): + while True: + if len(self._data) == 0: + self._r.select("搜索结果", ["未找到Snippet记录"]) + break + else: + # 将data按照索引和path换成dict,前者为键 + options = [f"{d.name} - {d.path}" for d in self._data] + index, key = self._r.select("搜索结果", options) + if key == -1: + break + SearchResultActionMenu(self._r, self._data[index]).run() + + +class SearchResultActionMenu: + def __init__(self, r: rofi.Rofi, data: SearchResponse.SearchData): + self._r = r + self.data = data + + def run(self): + while True: + index, key = self._r.select("操作", ["编辑", "编辑并复制", "删除"]) + if key == -1: + break + match index: + case 0: + result = edit(self.data.path, self.data.id) + if not result.ok: + self._r.error(result.message) + case 1: + result = edit_and_copy(self.data.path, self.data.id) + if not result.ok: + self._r.error(result.message) + exit() + case 2: + run_delete(self.data.path) + break diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..835e534 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Blair Bonnett + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..16efde9 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# CodeSnippetDaemon + +使用 Rofi 前端 + Java 守护进程管理代码片段,支持 Markdown 存储、跨平台同步和快速索引。 +## 项目概述 +- 使用`Rofi`前端+Java守护进程的代码片段管理工具。代码片段以Markdown形式存储在指定目录中,Java守护进程负责维护索引,Rofi前端负责与守护进程进行交互,并调用其他工具完成交互操作。 +- 该项目的设计目的在于,简化‘在线预览前端’的编写,可以通过其他工具查看实际存储的代码片段内容,同时又能够做到跨平台同步。 +- 因此,选择使用`Markdown`格式存储代码片段,使用SQLite维护目录索引,前端可以使用该项目列出的`Rofi`启动,也可以根据下文列出的请求格式自行编写前端。 +- 此外,由于守护进程体量确实不大,故未采用http服务器的形式,而是选择使用普通的TCP交互(即守护进程使用SocketServer,前端使用netcat)。 + +## 依赖(Rofi前端) +- rofi +- nc +- nvim + - 推荐装有 Markdown 相关插件 +- python3 +- python-rofi + +## 工作流程 + +### 守护进程 +- 实际代码片段内容存储在对应 Markdown 文件中 +- 守护进程通过 SQLite 数据库维护目录索引,提供异常处理机制维护索引一致性 +- 使用 SocketServer 创建 TCP 服务器,可由前端连接 +- 添加片段时使用 AI 提取提供的代码片段的部分信息(标签、描述等),省却部分手动操作,也可以在添加完毕后手动进行编辑 + +### 前端 +- 默认使用 Rofi 作为启动器,已涵盖后端所需的所有操作类型 +- 借助 Python 完成复杂操作 +- 涉及到代码片段的编辑行为时,将通过调用 nvim 来编辑临时文件,编辑完毕后将发送请求至守护进程 + +## 使用方法 +1. 设置环境变量 + - CODE_SNIPPET_CONF + - 配置文件目录 + - CODE_SNIPPET_DIR + - 文件存储目录 + - CODE_SNIPPET_PORT + - TCP服务器端口 + - CODE_SNIPPET_API_KEY + - AI服务商提供的的 api_key + - CODE_SNIPPET_BASE_URL + - AI服务商提供的 base_url + - CODE_SNIPPET_MODEL + - 所用模型名称 + - CODE_SNIPPET_ROFI (可选,若未指定则使用默认 rofi) + - 指定的 rofi 脚本,可以添加参数,比如: `rofi -theme ~/.config/rofi/launchers/type-4/style-1.rasi` + +2. 启动 Java 守护进程 +3. 启动 rofi 脚本 + + +## 说明 +### rofi 菜单说明 +- 搜索 + - 编辑 + - 编辑并复制 + - 删除 +- 添加 +- 编辑 +- 删除 + +### 接口说明 +详见: [接口说明](doc/接口说明.md) + +### 模板说明 +对文件做出修改、添加文件时,看到的文档内容、可填写的内容都以模板文档为基础。 + +详见: [模板说明](doc/模板说明.md) + +## 许可 +本项目使用了修改版的 python-rofi(原项目 [python-rofi](https://github.com/bcbnz/python-rofi),MIT License)。 + +对其源码进行了少量修改,以支持自定义 rofi 命令和样式。 + +本项目整体遵循 MIT 协议,详见 LICENSE 文件。 \ No newline at end of file diff --git a/doc/resource/add.png b/doc/resource/add.png new file mode 100644 index 0000000000000000000000000000000000000000..bc4b6d34f97c11e0a4385858110500f6126786d6 GIT binary patch literal 76705 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

h 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 CFRW4k literal 0 HcmV?d00001 diff --git a/doc/resource/empty-add.png b/doc/resource/empty-add.png new file mode 100644 index 0000000000000000000000000000000000000000..ca5a8696d18ad8b09c25c58eb786b8cec897e5db GIT binary patch literal 56274 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 该部分字段中,除`id`, `path`以外,都可进行编辑 + +#### 请求示例 +```json +{ + "action": "EDIT", + "data": { + "id": "1", //代码片段id + "path": "$CODE_SNIPPET_DIR/java/ExtractJson.md", //代码片段的实际存储路径 + "tags": "[JSON处理, 字符串操作, 文本提取]" //代码片段的标签 + "description": "从字符串中提取JSON内容的工具方法,支持处理中文引号并定位JSON对象的起始和结束位置", //代码片段描述 + "content": "public static String extractJson(String jsonStr) {\n jsonStr = jsonStr.replace(\"“\", \"\\\"\").replace(\"”\", \"\\\"\");\n int start = jsonStr.indexOf(\"{\");\n int end = jsonStr.lastIndexOf(\"}\");\n if (start != -1 && end != -1 && start < end) {\n return jsonStr.substring(start, end + 1);\n }\n return jsonStr;\n}" + } +} +``` + +#### 响应示例 +```json +{ + "data" : "文件编辑成功: /home/slhaf/Documents/code-snippet/java/ExtractJson.md", + "status" : "SUCCESS" +} +``` + +### 删除片段 +#### 请求示例 +```json +{ + "action": "DELETE", + "data": "$CODE_SNIPPET_DIR/java/ExtractJson.md" //待删除的文件 +} +``` + +#### 响应示例 +```json +{ + "data" : "删除成功: /home/slhaf/Documents/code-snippet/java/ExtractJson.md", + "status" : "SUCCESS" +} +``` +### 搜索片段 +> 搜索片段时,输入内容可由空格分隔开表明为多个匹配内容,可匹配`name`、`tags`、`description`、`language`多个字段,权重依次递减,分别为‘5、4、3、2、1’,匹配到多个字段时分值累加,最终返回的列表将依此进行由高到低的排序 + +#### 请求示例 +```json +{ + "action": "LIST", + "data": "json extract " +} +``` + +#### 响应示例 +```json +{ + "data" : "[{\"id\":\"1\",\"name\":\"ExtractJson\",\"path\":\"/home/slhaf/Documents/code-snippet/java/ExtractJson.md\",\"score\":14}]", + "status" : "SUCCESS" +} +``` \ No newline at end of file diff --git a/doc/模板说明.md b/doc/模板说明.md new file mode 100644 index 0000000..2dce026 --- /dev/null +++ b/doc/模板说明.md @@ -0,0 +1,34 @@ +## 模板说明 +### 添加片段 +```markdown + ## Snippet + + ```[Language] + <代码片段内容> + ``` + + ## MetaData + + - Name + - <代码片段名称> + - Language + - <代码片段所用语言> +``` + +### 编辑片段 +```markdown + ## Snippet + + ```[Language] + <代码片段内容> + ``` + + ## MetaData + + - Tags + - <代码片段标签> + - <可以为多个> + - <初次添加本片段时无需手动填写,由配置的LLM自动生成> + - Description + - <代码片段描述内容,默认也由LLM自动生成> +``` \ No newline at end of file diff --git a/test/data-test.json b/test/data-test.json new file mode 100644 index 0000000..730e069 --- /dev/null +++ b/test/data-test.json @@ -0,0 +1,8 @@ +{ + "action": "ADD", + "data": { + "name": "ExtractJSON", + "language": "Java", + "content": "public static String extractJson(String jsonStr) {\n jsonStr = jsonStr.replace(\"“\", \"\\\"\").replace(\"”\", \"\\\"\");\n int start = jsonStr.indexOf(\"{\");\n int end = jsonStr.lastIndexOf(\"}\");\n if (start != -1 && end != -1 && start < end) {\n return jsonStr.substring(start, end + 1);\n }\n return jsonStr;\n}" + } +} \ No newline at end of file diff --git a/test/test.json b/test/test.json new file mode 100644 index 0000000..c966901 --- /dev/null +++ b/test/test.json @@ -0,0 +1,7 @@ +{ + "tags": [ + "ByteBuddy", + "动态代理" + ], + "description": "通过ByteBuddy创建动态代理类的示例" +} \ No newline at end of file diff --git a/test/test.md b/test/test.md new file mode 100644 index 0000000..f0534ed --- /dev/null +++ b/test/test.md @@ -0,0 +1,50 @@ +## Snippet +```Java +class test { + while(true) + + { + //输入层级 + if (esc) { + break; + } + if (enter) { + //按下enter + while (true) { + //展示结果层 + if (未找到) { + //无结果 + if (esc || enter) { + break; + } + } else { + //有结果 + if (enter) { + //选中结果按下enter + while (true) { + //展示操作层 + if (esc) { + break; + } + if (enter) { + //执行操作 + doSomething(); + return; + } + } + } + } + } + } + } +} +``` + +## MetaData +- Language + - Java +- Tags + - hello world + - test +- Description + - test-description \ No newline at end of file