代码片段管理工具:rofi前端+Java守护进程
This commit is contained in:
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/CodeSnippetRofi/.idea/
|
||||
/CodeSnippetRofi/.venv/
|
||||
/CodeSnippetDaemon/.idea/
|
||||
/CodeSnippetDaemon/.mvn/
|
||||
/CodeSnippetDaemon/target/
|
||||
/CodeSnippetDaemon/dependency-reduced-pom.xml
|
||||
/CodeSnippetDaemon/pom.xml
|
||||
/.idea/
|
||||
98
CodeSnippetDaemon/src/main/java/work/slhaf/snippet/App.java
Normal file
98
CodeSnippetDaemon/src/main/java/work/slhaf/snippet/App.java
Normal file
@@ -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<String, String> sha2PathDB = indexManager.getIndexStatus();
|
||||
HashMap<String, String> sha2PathMD = snippetManager.getFileStatus();
|
||||
if (!sha2PathMD.equals(sha2PathDB)) {
|
||||
log.info("数据库与文件目录不一致,重建索引数据库");
|
||||
List<RebuildEntity> snippets = snippetManager.listAll();
|
||||
Set<Index> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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<Message> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
@@ -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<Message> 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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<ChoicesBean> 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;
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package work.slhaf.snippet.entity.file;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ListEntity implements Comparable<ListEntity> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package work.slhaf.snippet.entity.file;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class MetaDataEntity {
|
||||
private String description;
|
||||
private String[] tags;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<ListEntity> 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;
|
||||
}
|
||||
}
|
||||
@@ -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<Index> 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<ListEntity> list(String input) throws SQLException {
|
||||
String[] keywords = input.trim().toLowerCase().split("\\s+");
|
||||
|
||||
// 构建 SQL 占位符字符串
|
||||
StringBuilder sb = new StringBuilder("SELECT * FROM code_snippet_index WHERE ");
|
||||
List<String> 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<ListEntity> 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<String> 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<String, String> getIndexStatus() throws SQLException {
|
||||
Statement statement = connection.createStatement();
|
||||
ResultSet rs = statement.executeQuery("""
|
||||
select sha,path from code_snippet_index
|
||||
""");
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
while (rs.next()) {
|
||||
map.put(rs.getString("path"), rs.getString("sha"));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -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<Message> messages = List.of(
|
||||
new Message(ChatConstant.Character.SYSTEM, """
|
||||
你需要对接下来发送的代码相关内容进行提取,提取出‘描述’、‘标签’等内容。
|
||||
|
||||
输入格式示例:
|
||||
```json
|
||||
{
|
||||
"name": "ByteBuddy用法示例", //代码片段名称
|
||||
"language": "Java", //代码片段所属语言
|
||||
"content": "Class<? extends Module> clazz = module.getClazz();\\nClass<? extends Module> 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<Message> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<String, String> getFileStatus() {
|
||||
File file = new File(System.getenv(Constant.Property.DIR));
|
||||
HashMap<String, String> map = new HashMap<>();
|
||||
listFileStatus(file, map);
|
||||
return map;
|
||||
}
|
||||
|
||||
private void listFileStatus(File file, HashMap<String, String> 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<RebuildEntity> listAll() throws IOException {
|
||||
log.info("获取数据目录[{}]文件信息", System.getenv(Constant.Property.DIR));
|
||||
List<RebuildEntity> list = new ArrayList<>();
|
||||
File file = new File(System.getenv(Constant.Property.DIR));
|
||||
listAll(file, list);
|
||||
return list;
|
||||
}
|
||||
|
||||
private void listAll(File file, List<RebuildEntity> 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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
CodeSnippetDaemon/src/test/java/MarkdownTest.java
Normal file
11
CodeSnippetDaemon/src/test/java/MarkdownTest.java
Normal file
@@ -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);
|
||||
}
|
||||
29
CodeSnippetDaemon/src/test/java/SnippetReaderTest.java
Normal file
29
CodeSnippetDaemon/src/test/java/SnippetReaderTest.java
Normal file
@@ -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());
|
||||
}
|
||||
40
CodeSnippetRofi/common/constant.py
Normal file
40
CodeSnippetRofi/common/constant.py
Normal file
@@ -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
|
||||
-
|
||||
"""
|
||||
823
CodeSnippetRofi/common/rofi.py
Normal file
823
CodeSnippetRofi/common/rofi.py
Normal file
@@ -0,0 +1,823 @@
|
||||
#
|
||||
# python-rofi
|
||||
#
|
||||
# The MIT License
|
||||
#
|
||||
# Copyright (c) 2016, 2017 Blair Bonnett <blair.bonnett@gmail.com>
|
||||
#
|
||||
# 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("<b>{0:s}</b>: {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 = '<span color="#FF0000" font_weight="bold">{0:s}</span>\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)
|
||||
19
CodeSnippetRofi/entity/response.py
Normal file
19
CodeSnippetRofi/entity/response.py
Normal file
@@ -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"]
|
||||
4
CodeSnippetRofi/entity/result.py
Normal file
4
CodeSnippetRofi/entity/result.py
Normal file
@@ -0,0 +1,4 @@
|
||||
class ActionResult:
|
||||
def __init__(self, ok: bool, message: str):
|
||||
self.ok = ok
|
||||
self.message = message
|
||||
58
CodeSnippetRofi/helper/api_helper.py
Normal file
58
CodeSnippetRofi/helper/api_helper.py
Normal file
@@ -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))
|
||||
334
CodeSnippetRofi/helper/file_helper.py
Normal file
334
CodeSnippetRofi/helper/file_helper.py
Normal file
@@ -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<fence_lang>[^\]]*)\]|(?P<fence_lang2>[A-Za-z0-9_+\-]+))?\s*\r?\n(?P<code>[\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<meta>[\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)
|
||||
11
CodeSnippetRofi/launcher.py
Executable file
11
CodeSnippetRofi/launcher.py
Executable file
@@ -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()
|
||||
13
CodeSnippetRofi/menu/AddMenu.py
Normal file
13
CodeSnippetRofi/menu/AddMenu.py
Normal file
@@ -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)
|
||||
44
CodeSnippetRofi/menu/DeleteMenu.py
Normal file
44
CodeSnippetRofi/menu/DeleteMenu.py
Normal file
@@ -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)
|
||||
32
CodeSnippetRofi/menu/EditMenu.py
Normal file
32
CodeSnippetRofi/menu/EditMenu.py
Normal file
@@ -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)
|
||||
29
CodeSnippetRofi/menu/MainMenu.py
Normal file
29
CodeSnippetRofi/menu/MainMenu.py
Normal file
@@ -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()
|
||||
71
CodeSnippetRofi/menu/SearchMenu.py
Normal file
71
CodeSnippetRofi/menu/SearchMenu.py
Normal file
@@ -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
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -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.
|
||||
75
README.md
Normal file
75
README.md
Normal file
@@ -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 文件。
|
||||
BIN
doc/resource/add.png
Normal file
BIN
doc/resource/add.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 75 KiB |
BIN
doc/resource/empty-add.png
Normal file
BIN
doc/resource/empty-add.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 55 KiB |
81
doc/接口说明.md
Normal file
81
doc/接口说明.md
Normal file
@@ -0,0 +1,81 @@
|
||||
## 接口说明
|
||||
### 添加片段
|
||||
#### 请求示例
|
||||
```json
|
||||
{
|
||||
"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}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
```json
|
||||
{
|
||||
"data" : "代码片段已添加, 路径: /home/slhaf/Documents/code-snippet/java/ExtractJson.md",
|
||||
"status" : "SUCCESS"
|
||||
}
|
||||
```
|
||||
|
||||
### 编辑片段
|
||||
> 该部分字段中,除`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"
|
||||
}
|
||||
```
|
||||
34
doc/模板说明.md
Normal file
34
doc/模板说明.md
Normal file
@@ -0,0 +1,34 @@
|
||||
## 模板说明
|
||||
### 添加片段
|
||||
```markdown
|
||||
## Snippet
|
||||
|
||||
```[Language]
|
||||
<代码片段内容>
|
||||
```
|
||||
|
||||
## MetaData
|
||||
|
||||
- Name
|
||||
- <代码片段名称>
|
||||
- Language
|
||||
- <代码片段所用语言>
|
||||
```
|
||||
|
||||
### 编辑片段
|
||||
```markdown
|
||||
## Snippet
|
||||
|
||||
```[Language]
|
||||
<代码片段内容>
|
||||
```
|
||||
|
||||
## MetaData
|
||||
|
||||
- Tags
|
||||
- <代码片段标签>
|
||||
- <可以为多个>
|
||||
- <初次添加本片段时无需手动填写,由配置的LLM自动生成>
|
||||
- Description
|
||||
- <代码片段描述内容,默认也由LLM自动生成>
|
||||
```
|
||||
8
test/data-test.json
Normal file
8
test/data-test.json
Normal file
@@ -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}"
|
||||
}
|
||||
}
|
||||
7
test/test.json
Normal file
7
test/test.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"tags": [
|
||||
"ByteBuddy",
|
||||
"动态代理"
|
||||
],
|
||||
"description": "通过ByteBuddy创建动态代理类的示例"
|
||||
}
|
||||
50
test/test.md
Normal file
50
test/test.md
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user