[Feat] finished codesnap client

pull/3/head v0.0.2
Mist 2024-02-21 18:33:44 +08:00
parent f8773e538e
commit 383be3a533
19 changed files with 1756 additions and 114 deletions

View File

@ -1,15 +1,17 @@
local logger = require("codesnap.utils.logger")
local path_utils = require("codesnap.utils.path")
local static = require("codesnap.static")
local client = {
job_id = 0,
}
local cwd = static.cwd .. "/snap-server"
function client:connect()
return vim.fn.jobstart({
path_utils.back(path_utils.back(debug.getinfo(1, "S").source:sub(2):match("(.*[/\\])")))
.. "/snap-server/target/debug/snap-server",
cwd .. "/target/release/snap-server",
}, {
cwd = cwd,
stderr_buffered = true,
rpc = true,
on_stderr = function(_, err)

View File

@ -3,15 +3,14 @@ local static = require("codesnap.static")
local client = require("codesnap.client")
local visual_utils = require("codesnap.utils.visual")
local main = {}
local main = {
cwd = static.cwd,
preview_switch = static.preview_switch,
}
function main.setup(config)
static.config = table_utils.merge(static.config, config == nil and {} or config)
print(vim.inspect(static.config))
print(table_utils.serialize_json(static.config))
print()
if static.config.auto_load then
client:start()
end
@ -23,4 +22,12 @@ function main.preview_code()
client:send("preview_code", { content = visual_utils.get_selected_text(), language = vim.bo.filetype })
end
function main.open_preview()
client:send("open_preview")
end
function main.stop_client()
client:stop()
end
return main

View File

@ -1,11 +1,12 @@
local path_utils = require("codesnap.utils.path")
return {
config = {
breadcrumbs = true,
column_number = true,
mac_window_bar = true,
opacity = true,
watermark = "CodeSnap.nvim",
auto_load = true,
},
cwd = path_utils.back(path_utils.back(debug.getinfo(1, "S").source:sub(2):match("(.*[/\\])"))),
preview_switch = true,
}

20
plugin/build.lua Normal file
View File

@ -0,0 +1,20 @@
local codesnap = require("codesnap")
local snap_client_cwd = codesnap.cwd .. "/snap-client"
local snap_server_cwd = codesnap.cwd .. "/snap-server"
-- Build preview client
os.execute(
"cd "
.. snap_client_cwd
.. " && "
.. "npm i "
.. " && "
.. "npm run build"
.. " && "
.. "mv ./build "
.. snap_server_cwd
.. "/public"
)
-- Build server
os.execute("cd " .. snap_server_cwd .. " && " .. "cargo build --release")

View File

@ -1,19 +1,19 @@
local codesnap = require("codesnap")
local static = require("codesnap.static")
local client = require("codesnap.client")
-- local client = require("codesnap.client")
-- snap code
vim.api.nvim_create_user_command("CodeSnap", function() end, {})
-- vim.api.nvim_create_user_command("CodeSnap", function()
-- client:send("copy")
-- end, {})
vim.api.nvim_create_user_command("CodeSnapPreviewOn", function() end, {})
vim.api.nvim_create_user_command("CodeSnapPreviewOff", function() end, {})
vim.api.nvim_create_user_command("CodeSnapPreviewOn", function()
codesnap.open_preview()
end, {})
vim.api.nvim_create_autocmd({ "CursorMoved" }, {
callback = function()
local mode = vim.api.nvim_get_mode().mode
if mode ~= "v" or not static.preview_switch then
if mode ~= "v" or not codesnap.preview_switch then
return
end
@ -24,6 +24,6 @@ vim.api.nvim_create_autocmd({ "CursorMoved" }, {
vim.api.nvim_create_autocmd({ "VimLeavePre" }, {
pattern = "*",
callback = function()
client:stop()
codesnap.stop_client()
end,
})

View File

@ -1,34 +1,37 @@
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import useWebSocket, { ReadyState } from "react-use-websocket";
import { ControlBar, Editor, Frame, Panel } from "./components";
import { useConfig, useEvent } from "./hooks";
import { toPng, toJpeg, toBlob, toPixelData, toSvg } from "html-to-image";
import { toPng, toBlob } from "html-to-image";
import download from "downloadjs";
const CODE_EMPTY_PLACEHOLDER = `print "Hello, CodeSnap.nvim!"`;
function App() {
const [socketUrl] = useState("ws://127.0.0.1:8080/ws");
const [socketUrl] = useState(`ws://${window.location.host}/ws`);
const { sendMessage, lastMessage, readyState } = useWebSocket(socketUrl);
const event = useEvent(lastMessage);
const config = useConfig(event?.config_setup);
const frameRef = useRef<HTMLDivElement | null>(null);
const [isCopyButtonDisabled, setIsCopyButtonDisabled] = useState(false);
const handleCopyButtonClick = useCallback(async () => {
if (!frameRef.current) {
return;
}
const blob = await toBlob(frameRef.current);
const clipboardItem = new ClipboardItem({ "image/png": blob! });
setIsCopyButtonDisabled(true);
navigator.clipboard.write([clipboardItem]);
try {
const blob = await toBlob(frameRef.current);
const clipboardItem = new ClipboardItem({ "image/png": blob! });
navigator.clipboard.write([clipboardItem]);
} catch (e) {
console.error(e);
} finally {
setIsCopyButtonDisabled(false);
}
}, []);
const handleExportClick = useCallback(async () => {
@ -41,6 +44,24 @@ function App() {
download(dataURL, "codesnap.png");
}, []);
const notifyCopyCommand = useCallback(async () => {
if (!frameRef.current) {
return;
}
const dataURL = await toPng(frameRef.current);
sendMessage(dataURL);
}, [sendMessage]);
useEffect(() => {
if (readyState !== ReadyState.OPEN || !event?.copy) {
return;
}
notifyCopyCommand();
}, [event, readyState, notifyCopyCommand]);
return (
<div className="w-full h-full flex flex-col items-center bg-deep-gray">
<p className="rainbow-text text-4xl font-extrabold mt-20">
@ -48,11 +69,12 @@ function App() {
</p>
<Panel>
<ControlBar
isCopyButtonDisabled={isCopyButtonDisabled}
onExportClick={handleExportClick}
onCopyClick={handleCopyButtonClick}
readyState={readyState}
/>
<div className="rounded-xl overflow-hidden">
<div id="frame" className="rounded-xl overflow-hidden">
<Frame ref={frameRef} watermark={config?.watermark}>
<Editor
language={event?.code?.language}

View File

@ -0,0 +1,53 @@
import { useMemo } from "react";
import { ReadyState } from "react-use-websocket";
interface ConnectionStatusProps {
readyState: ReadyState;
}
const CONNECTION_STATUS_MAP = {
[ReadyState.CONNECTING]: {
text: "Connecting",
color: "#fdcb6e",
},
[ReadyState.OPEN]: {
text: "Connected",
color: "#00b894",
},
[ReadyState.CLOSING]: {
text: "Closing",
color: "#fab1a0",
},
[ReadyState.CLOSED]: {
text: "Closed",
color: "#636e72",
},
[ReadyState.UNINSTANTIATED]: {
text: "Uninstantiated",
color: "#2d3436",
},
} as const;
const UNKNOWN_STATE = { text: "Unknown", color: "#a29bfe" };
export const ConnectionStatus = ({ readyState }: ConnectionStatusProps) => {
const parsedState = useMemo(
() => CONNECTION_STATUS_MAP[readyState] ?? UNKNOWN_STATE,
[readyState],
);
return (
<div className="flex flex-grow flex-row items-center mr-1 ml-2">
<div
className="flex w-5 h-5 mr-2 justify-center items-center rounded-full"
style={{ backgroundColor: `${parsedState.color}50` }}
>
<div
className="w-3 h-3 rounded-full"
style={{ backgroundColor: parsedState.color }}
></div>
</div>
{parsedState.text}
</div>
);
};

View File

@ -2,6 +2,7 @@ import { ConnectionStatus } from "./connection-status";
import { ReadyState } from "react-use-websocket";
interface ControlBarProps {
isCopyButtonDisabled: boolean;
onCopyClick(): void;
onExportClick(): void;
readyState: ReadyState;
@ -10,13 +11,11 @@ interface ControlBarProps {
export const ControlBar = ({
onCopyClick,
onExportClick,
isCopyButtonDisabled,
readyState,
}: ControlBarProps) => {
return (
<div
className="bg-neutral rounded-xl mb-2 p-1 flex flex-row items-center"
onClick={onCopyClick}
>
<div className="bg-neutral rounded-xl mb-2 p-1 flex flex-row items-center">
<ConnectionStatus readyState={readyState} />
<div className="flex flex-row items-center">
{/*
@ -44,7 +43,11 @@ export const ControlBar = ({
<path d="M480-320 280-520l56-58 104 104v-326h80v326l104-104 56 58-200 200ZM240-160q-33 0-56.5-23.5T160-240v-120h80v120h480v-120h80v120q0 33-23.5 56.5T720-160H240Z" />
</svg>
</button>
<button className="btn">
<button
onClick={onCopyClick}
id="copy"
className={`btn ${isCopyButtonDisabled && "btn-disabled"}`}
>
Copy
<svg
className="fill-neutral-content"

View File

@ -1,8 +1,6 @@
import { useLocalStorage } from "./use-storage";
export interface Config {
breadcrumbs: boolean;
column_number: boolean;
mac_window_bar: boolean;
opacity: boolean;
watermark: string;

View File

@ -4,6 +4,7 @@ import { Config } from "./use-config";
export enum EventType {
CONFIG_SETUP = "config_setup",
CODE = "code",
COPY = "copy",
}
type CodeMessage = {
@ -14,6 +15,7 @@ type CodeMessage = {
type ParsedConfig = {
[EventType.CODE]: CodeMessage;
[EventType.CONFIG_SETUP]: Config;
[EventType.COPY]: undefined;
};
export const useEvent = (

1519
snap-server/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,13 +3,17 @@ name = "snap-server"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
actix = "0.13.3"
actix-files = "0.6.5"
actix-web = "4.5.1"
actix-web-actors = "4.3.0"
arboard = "3.3.1"
headless_chrome = "1.0.9"
image = "0.24.8"
image-base64 = "0.1.0"
neovim-lib = "0.6.1"
rand = "0.8.5"
serde = {version = "1.0.196", features = ["derive", "serde_derive", "std"]}
serde_json = "1.0.113"
webbrowser = "0.8.12"

View File

@ -0,0 +1,21 @@
use arboard::Clipboard;
use arboard::ImageData;
use image::load_from_memory;
pub fn copy_base64_image_into_clipboard(base64_image: String) {
copy_memory_image_into_clipboard(&image_base64::from_base64(base64_image))
}
pub fn copy_memory_image_into_clipboard(buffer: &Vec<u8>) {
let dynamic_image = load_from_memory(buffer).unwrap();
let image = ImageData {
width: dynamic_image.width() as usize,
height: dynamic_image.height() as usize,
bytes: dynamic_image.as_bytes().into(),
};
let mut clipboard = Clipboard::new().unwrap();
clipboard.set_image(image).unwrap();
}

View File

@ -3,15 +3,15 @@ pub mod config;
pub mod messages;
pub mod neovim;
use actix::{Actor, Addr, AsyncContext, Context};
use actix::{Actor, Addr, Context};
use arguments::parse_string_first;
pub use config::Config;
use headless_chrome::{protocol::cdp::Page, Browser, Tab};
pub use messages::Message;
use neovim::Neovim;
use serde_json::{json, Value};
use std::{
any::Any,
collections::HashMap,
error::Error,
sync::{Arc, Mutex},
};
@ -23,6 +23,7 @@ use crate::{
pub struct EventHandler {
neovim: Arc<Mutex<Neovim>>,
server: Arc<Addr<Server>>,
port: u16,
}
impl Actor for EventHandler {
@ -34,17 +35,47 @@ impl Actor for EventHandler {
}
impl EventHandler {
pub fn new(neovim: Arc<Mutex<Neovim>>, server: Arc<Addr<Server>>) -> EventHandler {
EventHandler { neovim, server }
pub fn new(neovim: Arc<Mutex<Neovim>>, server: Arc<Addr<Server>>, port: u16) -> EventHandler {
EventHandler {
neovim,
server,
port,
}
}
pub fn open_browser(&self) -> Result<Arc<Tab>, Box<dyn Error>> {
let browser = Browser::default()?;
let tab = browser.new_tab()?;
tab.navigate_to(format!("http://localhost:{}", self.port).as_str())?;
Ok(tab)
}
pub fn start_listen(&mut self) {
let receiver = self.neovim.lock().unwrap().create_receiver();
for (event_name, values) in receiver {
self.neovim.lock().unwrap().print(&event_name);
// self.neovim.lock().unwrap().print(&event_name);
match Message::from(event_name.clone()) {
Message::OpenPreview => {
let _ = webbrowser::open(format!("http://localhost:{}", self.port).as_str());
}
Message::Copy => {
// let _png_data = tab
// .wait_for_element("#frame")
// .unwrap()
// .capture_screenshot(Page::CaptureScreenshotFormatOption::Png)
// .unwrap();
//
// std::fs::write("hello_world.png", _png_data).unwrap();
// self.server.do_send(ClientMessage {
// msg: Event::new("copy", json!("{}")).into(),
// })
}
Message::PreviewCode => self.server.do_send(ClientMessage {
msg: Event::new(
"code",

View File

@ -2,8 +2,6 @@ use serde::{Deserialize, Serialize};
#[derive(Serialize, Debug, Deserialize, Clone)]
pub struct Config {
breadcrumbs: bool,
column_number: bool,
mac_window_bar: bool,
opacity: bool,
watermark: Option<String>,

View File

@ -2,6 +2,8 @@
pub enum Message {
PreviewCode,
ConfigSetup,
Copy,
OpenPreview,
Unknown,
}
@ -10,6 +12,8 @@ impl Eq for Message {}
impl From<String> for Message {
fn from(value: String) -> Self {
match value.as_str() {
"open_preview" => Message::OpenPreview,
"copy" => Message::Copy,
"preview_code" => Message::PreviewCode,
"config_setup" => Message::ConfigSetup,
_ => Message::Unknown,

View File

@ -1,18 +1,27 @@
mod clipboard;
mod event;
mod event_handler;
mod port;
mod server;
mod session;
use std::sync::{Arc, Mutex};
use std::{
net::TcpListener,
sync::{Arc, Mutex},
};
use actix::{Actor, Addr, Arbiter, StreamHandler};
use actix::{Actor, Addr, Arbiter};
use actix_files::{Files, NamedFile};
use actix_web::{
web::{self, Data, Payload},
App, Error, HttpRequest, HttpResponse, HttpServer,
App, Error, HttpRequest, HttpResponse, HttpServer, Responder,
};
use actix_web_actors::ws;
use event_handler::{neovim::Neovim, Config};
use event_handler::{EventHandler, Message};
use clipboard::copy_memory_image_into_clipboard;
use event_handler::neovim::Neovim;
use event_handler::EventHandler;
use headless_chrome::{protocol::cdp::Page, Browser, Tab};
use port::get_available_port;
use server::Server;
use session::Session;
@ -24,40 +33,33 @@ async fn index(
ws::start(Session::new(server), &req, stream)
}
async fn root() -> impl Responder {
NamedFile::open_async("./public/index.html").await.unwrap()
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let neovim = Arc::new(Mutex::new(Neovim::new()));
let server = Arc::new(Server::new(neovim.clone()).start());
let cloned_server = Arc::clone(&server);
let cloned_neovim = neovim.clone();
let available_port = get_available_port().unwrap();
Arbiter::new().spawn(async {
EventHandler::new(neovim, cloned_server).start();
Arbiter::new().spawn(async move {
EventHandler::new(cloned_neovim.clone(), cloned_server.clone(), available_port).start();
});
HttpServer::new(move || {
let _ = HttpServer::new(move || {
App::new()
.app_data(web::Data::new(server.clone()))
.route("/ws", web::get().to(index))
.service(web::resource("/").to(root))
.service(Files::new("/public", "./public"))
.service(Files::new("/static", "./public/static"))
})
.bind(("127.0.0.1", 8080))?
.bind(("127.0.0.1", available_port))?
.run()
.await
}
.await;
// fn main() {
// let data = r#"
// {
// "breadcrumbs":true,
// "watermark":"CodeSnap.nvim",
// "mac_window_bar":true,
// "column_number":true,
// "auto_load":true,
// "background":{
// "grandient":true
// }
// }"#;
//
// let config: Config = serde_json::from_str(data).unwrap();
//
// println!("{:?}", config)
// }
Ok(())
}

12
snap-server/src/port.rs Normal file
View File

@ -0,0 +1,12 @@
use std::net::TcpListener;
pub fn get_available_port() -> Option<u16> {
(8000..10000).find(|port| port_is_available(*port))
}
fn port_is_available(port: u16) -> bool {
match TcpListener::bind(("127.0.0.1", port)) {
Ok(_) => true,
Err(_) => false,
}
}

View File

@ -1,4 +1,4 @@
use crate::server::{ClientMessage, Connect, Disconnect, Server, ServerMessage};
use crate::server::{Connect, Disconnect, Server, ServerMessage};
use actix::{
dev::ContextFutureSpawner, fut, Actor, ActorContext, ActorFutureExt, Addr, AsyncContext,
Handler, Running, StreamHandler, WrapFuture,
@ -13,6 +13,10 @@ use std::{
const HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
const CLIENT_TIMEOUT: Duration = Duration::from_secs(30);
fn is_valid_base64_image(base64_image: &str) -> bool {
base64_image.starts_with("data:image/png;base64")
}
pub struct Session {
id: usize,
server: Arc<Addr<Server>>,
@ -30,7 +34,6 @@ impl Session {
fn heartbeat(&self, ctx: &mut ws::WebsocketContext<Self>) {
ctx.run_interval(HEARTBEAT_INTERVAL, |act, ctx| {
// check client heartbeats
if Instant::now().duration_since(act.heartbeat) > CLIENT_TIMEOUT {
act.server.do_send(Disconnect { id: act.id });
@ -89,9 +92,13 @@ impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Session {
self.heartbeat = Instant::now();
}
Ok(ws::Message::Text(text)) => {
self.server.do_send(ClientMessage {
msg: "aaaaa".to_string(),
});
// let image = text.to_string();
// if !is_valid_base64_image(&image) {
ctx.text(text)
// }
// copy_base64_image_into_clipboard(image);
}
Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
_ => (),