pbsync 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CLAUDE.md +174 -0
- data/LICENSE +21 -0
- data/README.md +229 -0
- data/bin/pbsync +6 -0
- data/lib/pbsync/cli.rb +25 -0
- data/lib/pbsync/client.rb +96 -0
- data/lib/pbsync/clipboard_sync.rb +144 -0
- data/lib/pbsync/server.rb +42 -0
- data/lib/pbsync/version.rb +5 -0
- data/lib/pbsync.rb +59 -0
- metadata +122 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: baac0be30f2e90c3fea252ccc0cf94d71f9e75b30574ebb41736d45d5c8f210d
|
4
|
+
data.tar.gz: 9e3cb20d0f47f90e15cebdead17a63b610ea9c83ec69f22314f827084b9371dd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 86d81c8534135c40f8231f2237057f35e952be688789ad45b59b6243a6ac7fbaf18173d01af2bae5949bad012425bc5c1a73e8dfded6e20e83f9258883cb62dd
|
7
|
+
data.tar.gz: '048c8c96c13c03ba4c81c1298579c82c18d4a0ff1f950b023d2af284a4eb8f9014506a2303fb7e109ffbb01b5b13cfbd9a91b6d674b983a52a101e8ed2d36e05'
|
data/CLAUDE.md
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
# CLAUDE.md
|
2
|
+
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
7
|
+
pbsync is a clipboard synchronization utility implemented as a Ruby gem. It allows bidirectional clipboard syncing between two machines over SSH using Unix domain sockets.
|
8
|
+
|
9
|
+
## Architecture
|
10
|
+
|
11
|
+
The application operates in two modes:
|
12
|
+
- **Server mode** (`pbsync serve`): Listens on a Unix socket at `/tmp/pbsync.sock` and accepts one client connection
|
13
|
+
- **Client mode** (`pbsync connect <hostname>`): Establishes SSH tunnel to remote host and connects to the server's socket
|
14
|
+
|
15
|
+
Key components:
|
16
|
+
- `PBSync::ClipboardSync`: Core synchronization logic that monitors clipboard changes and handles network communication
|
17
|
+
- `PBSync::Server`: Server implementation
|
18
|
+
- `PBSync::Client`: Client implementation with SSH tunneling
|
19
|
+
- `PBSync::CLI`: Command-line interface using Clamp gem for argument parsing
|
20
|
+
- Uses clipboard gem for cross-platform clipboard access
|
21
|
+
- Binary protocol with 4-byte length prefix (network byte order) followed by UTF-8 message data
|
22
|
+
- Maximum clipboard size: 1MB
|
23
|
+
- Polling interval: 0.5 seconds
|
24
|
+
|
25
|
+
## Ruby Gem Structure
|
26
|
+
|
27
|
+
```
|
28
|
+
pbsync/
|
29
|
+
├── bin/
|
30
|
+
│ └── pbsync # Executable script
|
31
|
+
├── lib/
|
32
|
+
│ ├── pbsync.rb # Main module
|
33
|
+
│ └── pbsync/
|
34
|
+
│ ├── version.rb
|
35
|
+
│ ├── cli.rb
|
36
|
+
│ ├── clipboard_sync.rb
|
37
|
+
│ ├── server.rb
|
38
|
+
│ └── client.rb
|
39
|
+
├── pbsync.gemspec # Gem specification
|
40
|
+
├── Gemfile
|
41
|
+
└── LICENSE
|
42
|
+
```
|
43
|
+
|
44
|
+
## Installation & Build Commands
|
45
|
+
|
46
|
+
```bash
|
47
|
+
# Install dependencies
|
48
|
+
bundle install
|
49
|
+
|
50
|
+
# Build the gem
|
51
|
+
gem build pbsync.gemspec
|
52
|
+
|
53
|
+
# Install the gem locally
|
54
|
+
gem install ./pbsync-0.1.0.gem
|
55
|
+
|
56
|
+
# Or install directly from source
|
57
|
+
bundle exec rake install
|
58
|
+
```
|
59
|
+
|
60
|
+
## Usage
|
61
|
+
|
62
|
+
```bash
|
63
|
+
# Run as server (on remote machine)
|
64
|
+
pbsync serve
|
65
|
+
pbsync serve --verbose
|
66
|
+
|
67
|
+
# Connect as client (on local machine)
|
68
|
+
pbsync connect <hostname>
|
69
|
+
pbsync connect <hostname> --verbose
|
70
|
+
|
71
|
+
# Show help
|
72
|
+
pbsync --help
|
73
|
+
pbsync serve --help
|
74
|
+
pbsync connect --help
|
75
|
+
```
|
76
|
+
|
77
|
+
## Testing
|
78
|
+
|
79
|
+
The gem includes comprehensive unit and integration tests using RSpec.
|
80
|
+
|
81
|
+
```bash
|
82
|
+
# Run all tests
|
83
|
+
bundle exec rake spec
|
84
|
+
|
85
|
+
# Run unit tests only
|
86
|
+
bundle exec rake unit
|
87
|
+
|
88
|
+
# Run integration tests only
|
89
|
+
bundle exec rake integration
|
90
|
+
|
91
|
+
# Run tests with coverage report
|
92
|
+
bundle exec rake coverage
|
93
|
+
|
94
|
+
# Run tests in verbose mode
|
95
|
+
bundle exec rake verbose
|
96
|
+
```
|
97
|
+
|
98
|
+
## Code Quality
|
99
|
+
|
100
|
+
The project uses RuboCop for Ruby code linting and style enforcement.
|
101
|
+
|
102
|
+
```bash
|
103
|
+
# Run RuboCop linter
|
104
|
+
bundle exec rake lint
|
105
|
+
|
106
|
+
# Run RuboCop with auto-correct
|
107
|
+
bundle exec rake lint_fix
|
108
|
+
|
109
|
+
# Run RuboCop directly with specific options
|
110
|
+
bundle exec rubocop
|
111
|
+
bundle exec rubocop --autocorrect-all
|
112
|
+
```
|
113
|
+
|
114
|
+
### RuboCop Configuration
|
115
|
+
|
116
|
+
- Target Ruby version: 2.7+
|
117
|
+
- Style guide: Standard Ruby with some customizations
|
118
|
+
- Enforced string literals: single quotes
|
119
|
+
- Max method length: 20 lines
|
120
|
+
- Max class length: 150 lines
|
121
|
+
- Line length: 120 characters (except in specs)
|
122
|
+
|
123
|
+
### Test Architecture
|
124
|
+
|
125
|
+
- **Unit Tests** (`spec/pbsync/`): Test individual classes with mocked dependencies
|
126
|
+
- `clipboard_sync_spec.rb`: Protocol handling, deduplication, thread safety
|
127
|
+
- `server_spec.rb`: Server socket creation and client handling
|
128
|
+
- `client_spec.rb`: SSH tunnel creation and connection management
|
129
|
+
|
130
|
+
- **Integration Tests** (`spec/integration/`): Test end-to-end socket communication
|
131
|
+
- Real Unix sockets in temp directories
|
132
|
+
- Bidirectional sync without SSH
|
133
|
+
- Protocol edge cases and error recovery
|
134
|
+
|
135
|
+
- **Test Helpers** (`spec/support/`):
|
136
|
+
- `MockClipboard`: Thread-safe clipboard mock for testing
|
137
|
+
- `MockSocket`: Simulated socket for unit testing
|
138
|
+
- `MockSocketPair`: Bidirectional socket pair for integration tests
|
139
|
+
|
140
|
+
## Logging
|
141
|
+
|
142
|
+
The gem uses Ruby's built-in Logger for output with sensible defaults:
|
143
|
+
|
144
|
+
- **Default log level**: INFO (shows important operational messages)
|
145
|
+
- **Verbose mode**: DEBUG (shows detailed operational information)
|
146
|
+
- **Output**: STDOUT with clean formatting (no timestamps for CLI usage)
|
147
|
+
- **Log levels**:
|
148
|
+
- INFO: Normal operational messages (no prefix)
|
149
|
+
- DEBUG: Detailed debugging info ([DEBUG] prefix)
|
150
|
+
- WARN: Warning messages ([WARN] prefix)
|
151
|
+
- ERROR: Error messages ([ERROR] prefix)
|
152
|
+
|
153
|
+
### Controlling Log Level
|
154
|
+
|
155
|
+
```bash
|
156
|
+
# Normal mode (INFO level)
|
157
|
+
pbsync connect hostname
|
158
|
+
|
159
|
+
# Verbose mode (DEBUG level)
|
160
|
+
pbsync connect hostname --verbose
|
161
|
+
pbsync serve -v
|
162
|
+
|
163
|
+
# In tests (suppressed by default)
|
164
|
+
DEBUG_TESTS=1 bundle exec rake spec # Show debug logs in tests
|
165
|
+
```
|
166
|
+
|
167
|
+
## Development Notes
|
168
|
+
|
169
|
+
- The client assumes `pbsync` is available in the remote machine's $PATH
|
170
|
+
- Socket path is hardcoded as `/tmp/pbsync.sock`
|
171
|
+
- Dependencies: clipboard gem for clipboard access, clamp gem for CLI parsing
|
172
|
+
- Works on macOS, Linux, and Windows (clipboard gem provides cross-platform support)
|
173
|
+
- Tests use dependency injection to mock clipboard and socket operations
|
174
|
+
- Logger outputs to STDOUT with clean formatting optimized for CLI usage
|
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
MIT License
|
2
|
+
|
3
|
+
Copyright (c) 2025 Remo
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
13
|
+
copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,229 @@
|
|
1
|
+
# PBSync
|
2
|
+
|
3
|
+
A Ruby gem for bidirectional clipboard synchronization between machines over SSH using Unix domain sockets.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **Bidirectional sync**: Changes on either machine are automatically synchronized
|
8
|
+
- **SSH tunneling**: Secure connection between machines
|
9
|
+
- **Cross-platform**: Works on macOS, Linux, and Windows (via clipboard gem)
|
10
|
+
- **Simple setup**: No configuration files needed
|
11
|
+
- **Automatic deduplication**: Prevents sync loops and duplicate transfers
|
12
|
+
- **UTF-8 support**: Handles international text and emoji correctly
|
13
|
+
- **Size limits**: Protects against oversized clipboard content (1MB max)
|
14
|
+
|
15
|
+
## Installation
|
16
|
+
|
17
|
+
### From RubyGems (when published)
|
18
|
+
|
19
|
+
```bash
|
20
|
+
gem install pbsync
|
21
|
+
```
|
22
|
+
|
23
|
+
### From Source
|
24
|
+
|
25
|
+
```bash
|
26
|
+
git clone https://github.com/yourusername/pbsync.git
|
27
|
+
cd pbsync
|
28
|
+
bundle install
|
29
|
+
gem build pbsync.gemspec
|
30
|
+
gem install ./pbsync-0.1.0.gem
|
31
|
+
```
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
### Basic Usage
|
36
|
+
|
37
|
+
Simply connect to a remote machine:
|
38
|
+
```bash
|
39
|
+
pbsync connect hostname
|
40
|
+
```
|
41
|
+
|
42
|
+
That's it! Your clipboards are now synchronized. Copy text on either machine and it will appear on the other.
|
43
|
+
|
44
|
+
The `connect` command automatically:
|
45
|
+
1. Establishes an SSH connection to the remote host
|
46
|
+
2. Starts the pbsync server on the remote machine
|
47
|
+
3. Creates a secure tunnel for clipboard synchronization
|
48
|
+
4. Begins monitoring both clipboards for changes
|
49
|
+
|
50
|
+
### Verbose Mode
|
51
|
+
|
52
|
+
For debugging or to see detailed operation logs:
|
53
|
+
```bash
|
54
|
+
pbsync connect hostname --verbose
|
55
|
+
# or
|
56
|
+
pbsync connect hostname -v
|
57
|
+
```
|
58
|
+
|
59
|
+
### Command Reference
|
60
|
+
|
61
|
+
```bash
|
62
|
+
# Show help
|
63
|
+
pbsync --help
|
64
|
+
|
65
|
+
# Connect to remote server (most common usage)
|
66
|
+
pbsync connect <hostname> [--verbose]
|
67
|
+
|
68
|
+
# Manually start server mode (rarely needed)
|
69
|
+
pbsync serve [--verbose]
|
70
|
+
|
71
|
+
# Show subcommand help
|
72
|
+
pbsync connect --help
|
73
|
+
pbsync serve --help
|
74
|
+
```
|
75
|
+
|
76
|
+
### Manual Server Mode
|
77
|
+
|
78
|
+
The `serve` command is available if you need to manually start a server, but this is rarely necessary since `connect` handles it automatically. You might use it for:
|
79
|
+
- Testing the server locally
|
80
|
+
- Running pbsync in a non-SSH environment
|
81
|
+
- Custom networking setups
|
82
|
+
|
83
|
+
## How It Works
|
84
|
+
|
85
|
+
1. **Server Mode**: Creates a Unix domain socket at `/tmp/pbsync.sock` and waits for connections
|
86
|
+
2. **Client Mode**: Establishes an SSH tunnel to the remote host, forwarding the Unix socket
|
87
|
+
3. **Monitoring**: Both sides poll their clipboard every 0.5 seconds for changes
|
88
|
+
4. **Synchronization**: When a change is detected, it's sent to the other side via the socket
|
89
|
+
5. **Deduplication**: Tracks sent and received content to prevent echo loops
|
90
|
+
|
91
|
+
### Protocol
|
92
|
+
|
93
|
+
PBSync uses a simple binary protocol:
|
94
|
+
- 4-byte length prefix (network byte order)
|
95
|
+
- UTF-8 encoded clipboard content
|
96
|
+
- Maximum message size: 1MB
|
97
|
+
|
98
|
+
## Requirements
|
99
|
+
|
100
|
+
- Ruby 2.7 or higher
|
101
|
+
- SSH access to the remote machine
|
102
|
+
- `pbsync` installed on both machines
|
103
|
+
- The remote machine must have `pbsync` in its `$PATH`
|
104
|
+
|
105
|
+
## Development
|
106
|
+
|
107
|
+
### Setup
|
108
|
+
|
109
|
+
```bash
|
110
|
+
# Clone the repository
|
111
|
+
git clone https://github.com/yourusername/pbsync.git
|
112
|
+
cd pbsync
|
113
|
+
|
114
|
+
# Install dependencies
|
115
|
+
bundle install
|
116
|
+
```
|
117
|
+
|
118
|
+
### Testing
|
119
|
+
|
120
|
+
```bash
|
121
|
+
# Run all tests
|
122
|
+
bundle exec rake spec
|
123
|
+
|
124
|
+
# Run with coverage report
|
125
|
+
bundle exec rake coverage
|
126
|
+
|
127
|
+
# Run specific test suites
|
128
|
+
bundle exec rake unit # Unit tests only
|
129
|
+
bundle exec rake integration # Integration tests only
|
130
|
+
```
|
131
|
+
|
132
|
+
### Code Quality
|
133
|
+
|
134
|
+
```bash
|
135
|
+
# Run RuboCop linter
|
136
|
+
bundle exec rake lint
|
137
|
+
|
138
|
+
# Auto-fix linting issues
|
139
|
+
bundle exec rake lint_fix
|
140
|
+
```
|
141
|
+
|
142
|
+
### Building
|
143
|
+
|
144
|
+
```bash
|
145
|
+
# Build the gem
|
146
|
+
gem build pbsync.gemspec
|
147
|
+
|
148
|
+
# Install locally for testing
|
149
|
+
gem install ./pbsync-0.1.0.gem
|
150
|
+
```
|
151
|
+
|
152
|
+
## Architecture
|
153
|
+
|
154
|
+
```
|
155
|
+
┌─────────────┐ ┌─────────────┐
|
156
|
+
│ Local │ │ Remote │
|
157
|
+
│ Machine │ │ Machine │
|
158
|
+
├─────────────┤ ├─────────────┤
|
159
|
+
│ Clipboard │◄──┐ ┌──►│ Clipboard │
|
160
|
+
└─────────────┘ │ │ └─────────────┘
|
161
|
+
▲ │ │ ▲
|
162
|
+
│ │ │ │
|
163
|
+
▼ │ │ ▼
|
164
|
+
┌─────────────┐ │ │ ┌─────────────┐
|
165
|
+
│ PBSync │ │ │ │ PBSync │
|
166
|
+
│ Client │ │ │ │ Server │
|
167
|
+
├─────────────┤ │ │ ├─────────────┤
|
168
|
+
│ Monitor & │ │ │ │ Monitor & │
|
169
|
+
│ Send/Recv │ │ │ │ Send/Recv │
|
170
|
+
└─────────────┘ │ │ └─────────────┘
|
171
|
+
▲ │ │ ▲
|
172
|
+
│ ▼ ▼ │
|
173
|
+
└──────────────────────────────────┘
|
174
|
+
SSH Tunnel + Unix Socket
|
175
|
+
|
176
|
+
```
|
177
|
+
|
178
|
+
## Troubleshooting
|
179
|
+
|
180
|
+
### Connection Issues
|
181
|
+
|
182
|
+
If you can't connect:
|
183
|
+
1. Ensure `pbsync` is installed on both machines
|
184
|
+
2. Verify SSH access: `ssh hostname`
|
185
|
+
3. Check that `pbsync` is in the remote machine's `$PATH`
|
186
|
+
4. Use verbose mode (`-v`) to see detailed logs
|
187
|
+
|
188
|
+
### Clipboard Not Syncing
|
189
|
+
|
190
|
+
1. Check that the clipboard contains text (not images or files)
|
191
|
+
2. Verify the content is under 1MB
|
192
|
+
3. Try verbose mode to see what's being sent/received
|
193
|
+
4. Ensure no other clipboard managers are interfering
|
194
|
+
|
195
|
+
### Performance
|
196
|
+
|
197
|
+
- The default polling interval is 0.5 seconds
|
198
|
+
- Large clipboard content (near 1MB) may take longer to sync
|
199
|
+
- Network latency affects synchronization speed
|
200
|
+
|
201
|
+
## Contributing
|
202
|
+
|
203
|
+
1. Fork the repository
|
204
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
205
|
+
3. Write tests for your changes
|
206
|
+
4. Ensure all tests pass (`bundle exec rake spec`)
|
207
|
+
5. Check code quality (`bundle exec rake lint`)
|
208
|
+
6. Commit your changes
|
209
|
+
7. Push to the branch (`git push origin feature/amazing-feature`)
|
210
|
+
8. Open a Pull Request
|
211
|
+
|
212
|
+
## License
|
213
|
+
|
214
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
215
|
+
|
216
|
+
## Acknowledgments
|
217
|
+
|
218
|
+
- Built with the [clipboard](https://github.com/janlelis/clipboard) gem for cross-platform clipboard access
|
219
|
+
- Uses [Clamp](https://github.com/mdub/clamp) for command-line argument parsing
|
220
|
+
- Inspired by the need for seamless clipboard sharing in remote development
|
221
|
+
|
222
|
+
## Changelog
|
223
|
+
|
224
|
+
### 0.1.0 (Initial Release)
|
225
|
+
|
226
|
+
- Basic bidirectional clipboard synchronization
|
227
|
+
- SSH tunneling support
|
228
|
+
- UTF-8 and size limit handling
|
229
|
+
- Logging system with verbose mode
|
data/bin/pbsync
ADDED
data/lib/pbsync/cli.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'clamp'
|
4
|
+
|
5
|
+
module PBSync
|
6
|
+
class CLI < Clamp::Command
|
7
|
+
option ['-v', '--verbose'], :flag, 'Enable verbose logging'
|
8
|
+
|
9
|
+
subcommand 'serve', 'Run in server mode' do
|
10
|
+
def execute
|
11
|
+
PBSync.verbose = verbose?
|
12
|
+
Server.new.run
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
subcommand 'connect', 'Connect and sync clipboard with host' do
|
17
|
+
parameter 'HOST', 'Hostname to connect to'
|
18
|
+
|
19
|
+
def execute
|
20
|
+
PBSync.verbose = verbose?
|
21
|
+
Client.new(host).run
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'open3'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
module PBSync
|
8
|
+
class Client
|
9
|
+
def initialize(host)
|
10
|
+
@host = host
|
11
|
+
end
|
12
|
+
|
13
|
+
def run
|
14
|
+
PBSync.logger.info("Entered client mode - host: #{@host}")
|
15
|
+
|
16
|
+
start_ssh_tunnel
|
17
|
+
wait_for_socket
|
18
|
+
connect_to_server
|
19
|
+
rescue Interrupt
|
20
|
+
PBSync.logger.info('Client interrupted')
|
21
|
+
rescue StandardError => e
|
22
|
+
PBSync.logger.error("Client error: #{e.message}")
|
23
|
+
PBSync.logger.debug(e.backtrace.join("\n")) if PBSync.verbose
|
24
|
+
ensure
|
25
|
+
cleanup
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def start_ssh_tunnel
|
31
|
+
remote_command = 'pbsync serve'
|
32
|
+
|
33
|
+
ssh_args = [
|
34
|
+
'ssh',
|
35
|
+
@host,
|
36
|
+
'-L', "#{SOCKET_PATH}:#{SOCKET_PATH}",
|
37
|
+
'sh', '-c', "'#{remote_command}'",
|
38
|
+
]
|
39
|
+
|
40
|
+
PBSync.log("Starting SSH tunnel with: #{ssh_args.join(" ")}")
|
41
|
+
|
42
|
+
@stdin, @stdout, @stderr, @ssh_thread = Open3.popen3(*ssh_args)
|
43
|
+
|
44
|
+
Thread.new do
|
45
|
+
while (line = @stderr.gets)
|
46
|
+
PBSync.log("SSH stderr: #{line.strip}")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
PBSync.logger.info('SSH tunnel launched')
|
51
|
+
end
|
52
|
+
|
53
|
+
def wait_for_socket
|
54
|
+
PBSync.logger.info('Waiting for socket to become available...')
|
55
|
+
|
56
|
+
30.times do
|
57
|
+
if File.exist?(SOCKET_PATH)
|
58
|
+
sleep 1
|
59
|
+
return
|
60
|
+
end
|
61
|
+
sleep 0.2
|
62
|
+
end
|
63
|
+
|
64
|
+
raise 'Failed to establish SSH tunnel - socket not created'
|
65
|
+
end
|
66
|
+
|
67
|
+
def connect_to_server
|
68
|
+
raise "Socket not found at #{SOCKET_PATH}" unless File.exist?(SOCKET_PATH)
|
69
|
+
|
70
|
+
PBSync.logger.info('Socket available, connecting...')
|
71
|
+
|
72
|
+
socket = UNIXSocket.new(SOCKET_PATH)
|
73
|
+
PBSync.logger.info("Connected to remote server via #{@host}")
|
74
|
+
|
75
|
+
sync = ClipboardSync.new(socket)
|
76
|
+
sync.start
|
77
|
+
|
78
|
+
PBSync.logger.info('Entering client loop')
|
79
|
+
|
80
|
+
Signal.trap('INT') do
|
81
|
+
PBSync.logger.info('Shutting down client...')
|
82
|
+
sync.stop
|
83
|
+
cleanup
|
84
|
+
exit(0)
|
85
|
+
end
|
86
|
+
|
87
|
+
sleep
|
88
|
+
end
|
89
|
+
|
90
|
+
def cleanup
|
91
|
+
@ssh_thread&.kill
|
92
|
+
[@stdin, @stdout, @stderr].each { |io| io&.close }
|
93
|
+
FileUtils.rm_f(SOCKET_PATH)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module PBSync
|
4
|
+
class ClipboardSync
|
5
|
+
attr_reader :running
|
6
|
+
|
7
|
+
def initialize(socket, clipboard_adapter: Clipboard)
|
8
|
+
@socket = socket
|
9
|
+
@clipboard = clipboard_adapter
|
10
|
+
@last_clipboard_sent = nil
|
11
|
+
@last_clipboard_received = nil
|
12
|
+
@running = true
|
13
|
+
end
|
14
|
+
|
15
|
+
def start
|
16
|
+
start_clipboard_monitor
|
17
|
+
start_receive_loop
|
18
|
+
end
|
19
|
+
|
20
|
+
def stop
|
21
|
+
@running = false
|
22
|
+
begin
|
23
|
+
@socket.close
|
24
|
+
rescue StandardError
|
25
|
+
nil
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def get_clipboard_text
|
32
|
+
text = @clipboard.paste
|
33
|
+
return nil if text.nil? || text.empty?
|
34
|
+
return nil if text.bytesize > MAX_SIZE
|
35
|
+
|
36
|
+
text
|
37
|
+
rescue StandardError => e
|
38
|
+
PBSync.log("Error reading clipboard: #{e.message}")
|
39
|
+
nil
|
40
|
+
end
|
41
|
+
|
42
|
+
def set_clipboard_text(text)
|
43
|
+
PBSync.log("Setting clipboard: #{text[0..99]}...")
|
44
|
+
@clipboard.copy(text)
|
45
|
+
rescue StandardError => e
|
46
|
+
PBSync.log("Error setting clipboard: #{e.message}")
|
47
|
+
end
|
48
|
+
|
49
|
+
def send_clipboard(text)
|
50
|
+
return unless text
|
51
|
+
|
52
|
+
data = text.encode('UTF-8')
|
53
|
+
length = [data.bytesize].pack('N')
|
54
|
+
packet = length + data
|
55
|
+
|
56
|
+
@socket.write(packet)
|
57
|
+
@socket.flush
|
58
|
+
PBSync.logger.info("📤 Sent clipboard (#{data.bytesize} bytes): \"#{text[0..99]}\"")
|
59
|
+
rescue StandardError => e
|
60
|
+
PBSync.log("Error sending clipboard: #{e.message}")
|
61
|
+
end
|
62
|
+
|
63
|
+
def read_exact(size)
|
64
|
+
data = ''
|
65
|
+
while data.bytesize < size
|
66
|
+
chunk = @socket.read(size - data.bytesize)
|
67
|
+
return nil if chunk.nil? || chunk.empty?
|
68
|
+
|
69
|
+
data += chunk
|
70
|
+
end
|
71
|
+
data
|
72
|
+
rescue StandardError => e
|
73
|
+
PBSync.log("Error reading from socket: #{e.message}")
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
def start_clipboard_monitor
|
78
|
+
PBSync.logger.info('📋 Starting clipboard monitor')
|
79
|
+
Thread.new do
|
80
|
+
while @running
|
81
|
+
begin
|
82
|
+
PBSync.log('⏱️ Clipboard poll fired')
|
83
|
+
text = get_clipboard_text
|
84
|
+
|
85
|
+
if text.nil?
|
86
|
+
PBSync.log('📋 Clipboard is empty or invalid')
|
87
|
+
elsif text == @last_clipboard_sent
|
88
|
+
PBSync.log('📋 Clipboard unchanged (already sent)')
|
89
|
+
elsif text == @last_clipboard_received
|
90
|
+
PBSync.log('📋 Clipboard came from remote (already received)')
|
91
|
+
else
|
92
|
+
PBSync.log('📋 Clipboard changed → sending')
|
93
|
+
@last_clipboard_sent = text
|
94
|
+
send_clipboard(text)
|
95
|
+
end
|
96
|
+
|
97
|
+
sleep POLL_INTERVAL
|
98
|
+
rescue StandardError => e
|
99
|
+
PBSync.log("Error in clipboard monitor: #{e.message}")
|
100
|
+
sleep POLL_INTERVAL
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def start_receive_loop
|
107
|
+
PBSync.logger.info('📥 Starting receive loop')
|
108
|
+
Thread.new do
|
109
|
+
while @running
|
110
|
+
begin
|
111
|
+
length_data = read_exact(4)
|
112
|
+
unless length_data && length_data.bytesize == 4
|
113
|
+
PBSync.logger.info('🔌 Disconnected (no length)')
|
114
|
+
break
|
115
|
+
end
|
116
|
+
|
117
|
+
length = length_data.unpack1('N')
|
118
|
+
message_data = read_exact(length)
|
119
|
+
unless message_data
|
120
|
+
PBSync.logger.info('Disconnected or bad message')
|
121
|
+
break
|
122
|
+
end
|
123
|
+
|
124
|
+
received = message_data.force_encoding('UTF-8')
|
125
|
+
|
126
|
+
if received != @last_clipboard_sent && received != @last_clipboard_received
|
127
|
+
@last_clipboard_received = received
|
128
|
+
set_clipboard_text(received)
|
129
|
+
PBSync.logger.info("📥 Received clipboard (#{message_data.bytesize} bytes): \"#{received[0..99]}\"")
|
130
|
+
else
|
131
|
+
PBSync.log('📥 Ignored duplicate clipboard content')
|
132
|
+
end
|
133
|
+
rescue StandardError => e
|
134
|
+
PBSync.log("Error in receive loop: #{e.message}")
|
135
|
+
break
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
stop
|
140
|
+
exit(0)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'fileutils'
|
5
|
+
|
6
|
+
module PBSync
|
7
|
+
class Server
|
8
|
+
def run
|
9
|
+
PBSync.logger.info('Entered server mode')
|
10
|
+
|
11
|
+
FileUtils.rm_f(SOCKET_PATH)
|
12
|
+
|
13
|
+
server = UNIXServer.new(SOCKET_PATH)
|
14
|
+
PBSync.logger.info("Server listening on #{SOCKET_PATH}")
|
15
|
+
|
16
|
+
client_socket = server.accept
|
17
|
+
PBSync.logger.info('Client connected')
|
18
|
+
|
19
|
+
sync = ClipboardSync.new(client_socket)
|
20
|
+
sync.start
|
21
|
+
|
22
|
+
PBSync.logger.info('Entering server loop')
|
23
|
+
|
24
|
+
Signal.trap('INT') do
|
25
|
+
PBSync.logger.info('Shutting down server...')
|
26
|
+
sync.stop
|
27
|
+
server.close
|
28
|
+
FileUtils.rm_f(SOCKET_PATH)
|
29
|
+
exit(0)
|
30
|
+
end
|
31
|
+
|
32
|
+
sleep
|
33
|
+
rescue Interrupt
|
34
|
+
PBSync.logger.info('Server interrupted')
|
35
|
+
rescue StandardError => e
|
36
|
+
PBSync.logger.error("Server error: #{e.message}")
|
37
|
+
PBSync.logger.debug(e.backtrace.join("\n")) if PBSync.verbose
|
38
|
+
ensure
|
39
|
+
FileUtils.rm_f(SOCKET_PATH)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/pbsync.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'socket'
|
4
|
+
require 'clipboard'
|
5
|
+
require 'timeout'
|
6
|
+
require 'logger'
|
7
|
+
require_relative 'pbsync/version'
|
8
|
+
require_relative 'pbsync/clipboard_sync'
|
9
|
+
require_relative 'pbsync/server'
|
10
|
+
require_relative 'pbsync/client'
|
11
|
+
require_relative 'pbsync/cli'
|
12
|
+
|
13
|
+
module PBSync
|
14
|
+
SOCKET_PATH = '/tmp/pbsync.sock'
|
15
|
+
MAX_SIZE = 1_000_000
|
16
|
+
POLL_INTERVAL = 0.5
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def logger
|
20
|
+
@logger ||= create_logger
|
21
|
+
end
|
22
|
+
|
23
|
+
attr_writer :logger
|
24
|
+
|
25
|
+
def verbose=(value)
|
26
|
+
logger.level = value ? Logger::DEBUG : Logger::INFO
|
27
|
+
end
|
28
|
+
|
29
|
+
def verbose
|
30
|
+
logger.level == Logger::DEBUG
|
31
|
+
end
|
32
|
+
|
33
|
+
# Compatibility method for existing code
|
34
|
+
def log(message)
|
35
|
+
logger.debug(message)
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def create_logger
|
41
|
+
log = Logger.new($stdout)
|
42
|
+
log.level = Logger::INFO
|
43
|
+
log.formatter = proc do |severity, _datetime, _progname, msg|
|
44
|
+
# Simple format without timestamp for cleaner CLI output
|
45
|
+
case severity
|
46
|
+
when 'DEBUG'
|
47
|
+
"[DEBUG] #{msg}\n"
|
48
|
+
when 'WARN'
|
49
|
+
"[WARN] #{msg}\n"
|
50
|
+
when 'ERROR'
|
51
|
+
"[ERROR] #{msg}\n"
|
52
|
+
else
|
53
|
+
"#{msg}\n"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
log
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
metadata
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pbsync
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Remo
|
8
|
+
bindir: bin
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: clipboard
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '1.3'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '1.3'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: clamp
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - "~>"
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '1.3'
|
33
|
+
type: :runtime
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - "~>"
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '1.3'
|
40
|
+
- !ruby/object:Gem::Dependency
|
41
|
+
name: logger
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
43
|
+
requirements:
|
44
|
+
- - "~>"
|
45
|
+
- !ruby/object:Gem::Version
|
46
|
+
version: '1.5'
|
47
|
+
type: :runtime
|
48
|
+
prerelease: false
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - "~>"
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '1.5'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: rake
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '13.0'
|
61
|
+
type: :development
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '13.0'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: rspec
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - "~>"
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '3.0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - "~>"
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '3.0'
|
82
|
+
description: Bidirectional clipboard syncing between machines over SSH using Unix
|
83
|
+
sockets
|
84
|
+
executables:
|
85
|
+
- pbsync
|
86
|
+
extensions: []
|
87
|
+
extra_rdoc_files: []
|
88
|
+
files:
|
89
|
+
- CLAUDE.md
|
90
|
+
- LICENSE
|
91
|
+
- README.md
|
92
|
+
- bin/pbsync
|
93
|
+
- lib/pbsync.rb
|
94
|
+
- lib/pbsync/cli.rb
|
95
|
+
- lib/pbsync/client.rb
|
96
|
+
- lib/pbsync/clipboard_sync.rb
|
97
|
+
- lib/pbsync/server.rb
|
98
|
+
- lib/pbsync/version.rb
|
99
|
+
homepage: https://github.com/sudoremo/pbsync
|
100
|
+
licenses:
|
101
|
+
- MIT
|
102
|
+
metadata:
|
103
|
+
homepage_uri: https://github.com/sudoremo/pbsync
|
104
|
+
source_code_uri: https://github.com/sudoremo/pbsync
|
105
|
+
rdoc_options: []
|
106
|
+
require_paths:
|
107
|
+
- lib
|
108
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
109
|
+
requirements:
|
110
|
+
- - ">="
|
111
|
+
- !ruby/object:Gem::Version
|
112
|
+
version: 2.7.0
|
113
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
requirements: []
|
119
|
+
rubygems_version: 3.6.8
|
120
|
+
specification_version: 4
|
121
|
+
summary: Clipboard synchronization utility for macOS
|
122
|
+
test_files: []
|