Skip to content

Commit

Permalink
Merge pull request #1 from renxida/new-architecture
Browse files Browse the repository at this point in the history
Implement hash and timestamp-based syncing
  • Loading branch information
renxida authored Jun 28, 2024
2 parents f5fe27a + 519ff1a commit a173733
Show file tree
Hide file tree
Showing 3 changed files with 122 additions and 89 deletions.
68 changes: 49 additions & 19 deletions uniclip.lua
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,18 @@ local function make_request(method, url, body)
return result
end

-- Hash content
local function hash_content(content)
return vim.fn.sha256(content)
end

-- Get current timestamp
local function get_timestamp()
return os.time()
end

-- Send content to server
function M.send_to_server(content)
function M.send_to_server(content, timestamp)
local group_id, server_address = load_config()
local temp_file = os.tmpname()

Expand All @@ -64,7 +74,12 @@ function M.send_to_server(content)
log_error("Failed to create temporary file")
return
end
f:write(content)
f:write(vim.fn.json_encode({
group_id = group_id,
client_id = "neovim-client",
content = content,
timestamp = timestamp
}))
f:close()

local curl_command = string.format(
Expand All @@ -88,34 +103,40 @@ function M.send_to_server(content)
end

-- Receive content from server
function M.receive_from_server()
function M.receive_from_server(content_hash, timestamp)
local group_id, server_address = load_config()
local response = make_request("GET", server_address .. "/poll/" .. group_id .. "/neovim-client")
local url = string.format("%s/poll/%s/neovim-client?hash=%s&timestamp=%d", server_address, group_id, content_hash, timestamp)
local response = make_request("GET", url)
local success, data = pcall(vim.fn.json_decode, response)
if not success then
log_error("Failed to decode server response: " .. response)
return nil
end
return data and data.content
return data
end

-- Clipboard sync functions

local last_clipboard = ""
local last_timestamp = 0
local current_backoff = poll_interval

-- Update clipboard with exponential backoff
local function update_clipboard()
local content = M.receive_from_server()
if content then
if content ~= last_clipboard then
vim.fn.setreg('"', content)
last_clipboard = content
local content_hash = hash_content(last_clipboard)
local data = M.receive_from_server(content_hash, last_timestamp)
if data then
if data.status == "update_needed" then
if data.content ~= last_clipboard then
vim.fn.setreg('"', data.content)
last_clipboard = data.content
last_timestamp = data.timestamp
end
end
current_backoff = poll_interval -- Reset backoff on successful poll
else
log_error("Failed to receive update from server. Retrying in " .. current_backoff / 1000 .. " seconds.")
current_backoff = math.min(current_backoff * 4, max_backoff) -- Exponential backoff with a factor of 4
current_backoff = math.min(current_backoff * 2, max_backoff) -- Exponential backoff
end
end

Expand All @@ -136,16 +157,20 @@ function M.setup()
group = vim.api.nvim_create_augroup("Uniclip", { clear = true }),
callback = function()
local yanked_text = table.concat(vim.v.event.regcontents, "\n")
M.send_to_server(yanked_text)
local timestamp = get_timestamp()
M.send_to_server(yanked_text, timestamp)
last_clipboard = yanked_text
last_timestamp = timestamp
end,
})

vim.keymap.set('n', 'p', function()
local content = M.receive_from_server()
if content and content ~= last_clipboard then
vim.fn.setreg('"', content)
last_clipboard = content
local content_hash = hash_content(last_clipboard)
local data = M.receive_from_server(content_hash, last_timestamp)
if data and data.status == "update_needed" and data.content ~= last_clipboard then
vim.fn.setreg('"', data.content)
last_clipboard = data.content
last_timestamp = data.timestamp
end
return 'p'
end, { expr = true })
Expand All @@ -156,19 +181,24 @@ end
-- Function to check Uniclip status
function M.check_status()
local group_id, server_address = load_config()
local response = make_request("GET", server_address .. "/poll/" .. group_id .. "/neovim-client")
local content_hash = hash_content(last_clipboard)
local response = make_request("GET", string.format("%s/poll/%s/neovim-client?hash=%s&timestamp=%d", server_address, group_id, content_hash, last_timestamp))
local success, data = pcall(vim.fn.json_decode, response)
if success and data then
print("Uniclip server is running and reachable.")
print("Server address: " .. server_address)
print("Group ID: " .. group_id)
print("Current backoff: " .. current_backoff / 1000 .. " seconds")
print("Last received content: " .. vim.inspect(data.content))
print("Last clipboard content hash: " .. content_hash)
print("Last clipboard timestamp: " .. os.date("%Y-%m-%d %H:%M:%S", last_timestamp))
print("Server response: " .. vim.inspect(data))
else
print("Unable to reach Uniclip server or server is not responding correctly.")
print("Server address: " .. server_address)
print("Group ID: " .. group_id)
print("Current backoff: " .. current_backoff / 1000 .. " seconds")
print("Last clipboard content hash: " .. content_hash)
print("Last clipboard timestamp: " .. os.date("%Y-%m-%d %H:%M:%S", last_timestamp))
print("Server response: " .. vim.inspect(response))
end
end
Expand Down Expand Up @@ -210,4 +240,4 @@ end, {
end,
})

return M
return M
78 changes: 47 additions & 31 deletions uniclip/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import pyperclip
import socket
import uuid
import hashlib

class Client:
# Change: Added force_headless parameter to __init__
# Added timestamp to __init__
def __init__(self, group_id, server_address, logger, force_headless=False):
self.group_id = group_id
self.server_address = server_address
Expand All @@ -16,6 +18,8 @@ def __init__(self, group_id, server_address, logger, force_headless=False):
self.force_headless = force_headless
self.headless = self._detect_headless()
self.client_id = self._generate_client_id()
self.last_clipboard = ''
self.last_timestamp = 0
self.logger.debug(f"Client initialized with group_id: {group_id}, server_address: {server_address}, client_id: {self.client_id}, force_headless: {force_headless}")

def _generate_client_id(self):
Expand All @@ -24,7 +28,6 @@ def _generate_client_id(self):
return f"{hostname}-{short_uuid}"

def _detect_headless(self):
# Change: Use force_headless if set
if self.force_headless:
self.logger.info("Forced headless mode")
return True
Expand All @@ -46,14 +49,14 @@ def run(self):
threading.Thread(target=self.monitor_clipboard, daemon=True).start()
self.register_with_server()

# Keep the main thread running
try:
while self.running:
time.sleep(1)
except KeyboardInterrupt:
self.logger.info("Stopping client...")
self.running = False

# Modified to use file's last modified time
def monitor_clipboard_file(self):
last_modified = 0
self.logger.debug(f"Starting to monitor clipboard file: {self.clipboard_file}")
Expand All @@ -64,33 +67,37 @@ def monitor_clipboard_file(self):
last_modified = current_modified
with open(self.clipboard_file, 'r') as f:
content = f.read()
self.logger.debug(f"Clipboard file changed. New content: {content[:50]}...")
self.send_to_server(content)
if content != self.last_clipboard:
self.logger.debug(f"Clipboard file changed. New content: {content[:50]}...")
self.last_clipboard = content
self.last_timestamp = int(current_modified)
self.send_to_server(content, self.last_timestamp)
except FileNotFoundError:
self.logger.debug(f"Clipboard file not found. Creating: {self.clipboard_file}")
open(self.clipboard_file, 'a').close()
except Exception as e:
self.logger.error(f"Error monitoring clipboard file: {e}")
time.sleep(0.5) # Check file every 0.5 seconds
time.sleep(0.5)

# Modified to record timestamp when copying
def monitor_clipboard(self):
last_clipboard = ''
self.logger.debug("Starting to monitor system clipboard")
while self.running:
try:
current_clipboard = pyperclip.paste()
if current_clipboard != last_clipboard:
if current_clipboard != self.last_clipboard:
self.logger.debug(f"Clipboard content changed. New content: {current_clipboard[:50]}...")
last_clipboard = current_clipboard
self.send_to_server(current_clipboard)
self.last_clipboard = current_clipboard
self.last_timestamp = int(time.time())
self.send_to_server(current_clipboard, self.last_timestamp)
except pyperclip.PyperclipException as e:
self.logger.error(f"Error accessing clipboard: {e}")
self.logger.info("Switching to headless mode")
self.headless = True
self.logger.debug("Starting clipboard file monitoring thread")
threading.Thread(target=self.monitor_clipboard_file, daemon=True).start()
break
time.sleep(0.5) # Check clipboard every 0.5 seconds
time.sleep(0.5)

def register_with_server(self):
self.logger.debug(f"Attempting to register with server: {self.server_address}")
Expand All @@ -108,45 +115,54 @@ def register_with_server(self):
except requests.RequestException as e:
self.logger.error(f"Error connecting to server: {e}")

# Modified to include content hash and timestamp
def poll_server(self):
self.logger.debug("Starting to poll server for updates")
while self.running:
try:
content_hash = hashlib.md5(self.last_clipboard.encode()).hexdigest()
self.logger.debug(f"Polling server: {self.server_address}/poll/{self.group_id}/{self.client_id}")
response = requests.get(f"{self.server_address}/poll/{self.group_id}/{self.client_id}")
response = requests.get(f"{self.server_address}/poll/{self.group_id}/{self.client_id}",
params={"hash": content_hash, "timestamp": self.last_timestamp})
if response.status_code == 200:
data = response.json()
clipboard_content = data.get('content')
if clipboard_content:
self.logger.debug(f"Received new content from server: {clipboard_content[:50]}...")
if self.headless:
self.update_clipboard_file(clipboard_content)
else:
try:
pyperclip.copy(clipboard_content)
self.logger.debug("Successfully copied new content to clipboard")
except pyperclip.PyperclipException as e:
self.logger.error(f"Error copying to clipboard: {e}")
self.logger.info("Switching to headless mode")
self.headless = True
if data.get('status') == 'update_needed':
clipboard_content = data.get('content')
timestamp = data.get('timestamp')
if clipboard_content and timestamp:
self.logger.debug(f"Received new content from server: {clipboard_content[:50]}...")
if self.headless:
self.update_clipboard_file(clipboard_content)
self.logger.info("Received new clipboard content from server")
else:
try:
pyperclip.copy(clipboard_content)
self.logger.debug("Successfully copied new content to clipboard")
except pyperclip.PyperclipException as e:
self.logger.error(f"Error copying to clipboard: {e}")
self.logger.info("Switching to headless mode")
self.headless = True
self.update_clipboard_file(clipboard_content)
self.last_clipboard = clipboard_content
self.last_timestamp = timestamp
self.logger.info("Received new clipboard content from server")
else:
self.logger.debug("No new content received from server")
else:
self.logger.warning(f"Unexpected status code from server: {response.status_code}")
time.sleep(0.5) # Poll every 0.5 seconds
time.sleep(0.5)
except requests.RequestException as e:
self.logger.error(f"Error polling server: {e}")
time.sleep(5) # Wait 5 seconds before retrying
time.sleep(5)

def send_to_server(self, content):
# Modified to include timestamp
def send_to_server(self, content, timestamp):
self.logger.debug(f"Sending update to server: {content[:50]}...")
try:
response = requests.post(f"{self.server_address}/update", json={
"group_id": self.group_id,
"client_id": self.client_id,
"content": content
"content": content,
"timestamp": timestamp
})
if response.status_code == 200:
self.logger.info(f"Sent update to server: {content[:20]}...")
Expand All @@ -162,4 +178,4 @@ def update_clipboard_file(self, content):
f.write(content)
self.logger.debug("Clipboard file updated successfully")
except Exception as e:
self.logger.error(f"Error updating clipboard file: {e}")
self.logger.error(f"Error updating clipboard file: {e}")
Loading

0 comments on commit a173733

Please sign in to comment.