Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement hash and timestamp-based syncing #1

Merged
merged 1 commit into from
Jun 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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