Compare commits

...

9 Commits

Author SHA1 Message Date
cd26941b51 fix(deps): update rust crate uuid to v1.13.1
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-05 06:25:49 +00:00
7789441121 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-02-04 02:23:59 +00:00
e359567e7d fix(deps): update rust crate uuid to v1.12.0
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2025-01-15 02:25:40 +00:00
dbe2d1fc7a
chore: fix demo
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-14 08:35:14 +01:00
aaba4ed411
feat: add demo gif
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-14 08:34:46 +01:00
153edb10d4
docs: add more thorough example
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-14 08:22:29 +01:00
37df2dcc6d
docs: correct docs 2025-01-14 08:19:39 +01:00
881094e484
feat: add basic remote copu
All checks were successful
continuous-integration/drone/push Build is passing
2025-01-14 08:16:50 +01:00
1b805ebf89 chore(release): v0.0.1 (#3)
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is passing
chore(release): 0.0.1

Co-authored-by: cuddle-please <bot@cuddle.sh>
Reviewed-on: #3
2025-01-10 09:40:27 +01:00
15 changed files with 1490 additions and 22 deletions

20
CHANGELOG.md Normal file
View File

@ -0,0 +1,20 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.0.1] - 2025-01-10
### Added
- add initial copy command
### Docs
- add example of call
- add a small readme with intent
### Other
- add stub for remote copier
- extract local copier to struct

831
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"
[workspace.package]
version = "0.1.0"
version = "0.0.1"
[workspace.dependencies]
voidpin = { path = "crates/voidpin" }

View File

@ -4,9 +4,25 @@ Voidpin allows sending copy/paste commands across the wire. It is specifically i
Voidpin sends clipboard content to a local service, that then puts content in a local clipboard. It can also go the other way, but the primary intent is remote -> local.
## Demo
![demo](assets/demo.gif)
## Usage
```bash
# Local
voidpin copy
VOIDPIN_REMOTE=10.0.9.1:19191 voidpin remote copy
# Remote
ip a # 10.0.9.1
voidpin listen & # make voidpin listener run in background
# login to remote
ssh remote@remote
export VOIDPIN_REMOTE=http://10.0.9.1:7900
echo "some content" | voidpin remote copy
# In a browser for example
# Ctrl+v or Command+v
```

175
assets/demo.cast Normal file
View File

@ -0,0 +1,175 @@
{"version": 2, "width": 130, "height": 40, "timestamp": 1736839984, "env": {"SHELL": "/bin/zsh", "TERM": "xterm-256color"}}
[0.479638, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[0.586074, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[38;2;255;153;102mvoidpin\u001b[0m \u001b[90mHEAD\u001b[1;32m(153edb1)\u001b[0m \u001b[38;2;255;153;102m \u001b[0mis \u001b[1;38;5;208m📦 v0.0.1\u001b[0m \u001b[1;31mrs \u001b[0mwith \u001b[1;34m🐃 v1.49.0 \u001b[0m\r\n\u001b[38;2;255;153;102m\u001b[0m \u001b[K"]
[0.587071, "o", "\u001b[6 q"]
[0.587964, "o", "\u001b[6 q"]
[0.588147, "o", "\u001b[?2004h"]
[3.167878, "o", "v"]
[3.191237, "o", "\b\u001b[1m\u001b[31mv\u001b[0m\u001b[39m"]
[3.232572, "o", "\b\u001b[1m\u001b[31mv\u001b[0m\u001b[39m\u001b[90moidpin listen\u001b[39m\u001b[13D"]
[3.300942, "o", "\b\u001b[1m\u001b[31mv\u001b[1m\u001b[31mo\u001b[0m\u001b[39m"]
[3.430551, "o", "\b\b\u001b[1m\u001b[31mv\u001b[1m\u001b[31mo\u001b[1m\u001b[31mi\u001b[0m\u001b[39m"]
[3.544553, "o", "\b\u001b[1m\u001b[31mi\u001b[1m\u001b[31md\u001b[0m\u001b[39m"]
[3.689365, "o", "\b\u001b[1m\u001b[31md\u001b[1m\u001b[31mp\u001b[0m\u001b[39m"]
[3.797695, "o", "\b\u001b[1m\u001b[31mp\u001b[1m\u001b[31mi\u001b[0m\u001b[39m"]
[3.97905, "o", "\b\u001b[1m\u001b[31mi\u001b[1m\u001b[31mn\u001b[0m\u001b[39m"]
[3.980916, "o", "\b\b\b\b\b\b\b\u001b[0m\u001b[32mv\u001b[0m\u001b[32mo\u001b[0m\u001b[32mi\u001b[0m\u001b[32md\u001b[0m\u001b[32mp\u001b[0m\u001b[32mi\u001b[0m\u001b[32mn\u001b[39m"]
[4.066146, "o", "\b\u001b[32mn\u001b[32m \u001b[39m"]
[4.068196, "o", "\b\b\u001b[32mn\u001b[39m\u001b[39m "]
[4.336382, "o", "\u001b[39mr\u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \b\b\b\b\b"]
[4.342211, "o", "\b\u001b[4mr\u001b[24m"]
[4.422896, "o", "\b\u001b[4mr\u001b[4me\u001b[24m"]
[4.452272, "o", "\b\u001b[4me\u001b[4mm\u001b[24m"]
[4.45517, "o", "\b\b\b\u001b[24mr\u001b[24me\u001b[24mm"]
[4.583628, "o", "o"]
[4.666593, "o", "t"]
[4.759127, "o", "e"]
[4.900598, "o", " "]
[5.746797, "o", "&"]
[6.624085, "o", "\b \b"]
[6.74323, "o", "\b"]
[6.896869, "o", "\b \b"]
[7.033699, "o", "\b \b"]
[7.174094, "o", "\b \b"]
[7.309328, "o", "\b \b"]
[7.313831, "o", "\b\b\u001b[4mr\u001b[4me\u001b[24m"]
[7.443394, "o", "\b\b\u001b[4mr\u001b[24m\u001b[24m \b"]
[7.573529, "o", "\b\u001b[24m \b"]
[7.600586, "o", "\u001b[90mlisten\u001b[39m\b\b\b\b\b\b"]
[7.789065, "o", "\u001b[39ml"]
[7.957613, "o", "\u001b[39mi"]
[8.041393, "o", "\u001b[39ms"]
[8.137227, "o", "\u001b[39mt"]
[8.196626, "o", "\u001b[39me"]
[8.263729, "o", "\u001b[39mn"]
[8.401872, "o", " "]
[8.5879, "o", "&"]
[8.95472, "o", "\u001b[?1l\u001b>"]
[8.954912, "o", "\u001b[?2004l"]
[8.960125, "o", "\u001b[0 q"]
[8.960423, "o", "\r\r\n"]
[8.986315, "o", "[1] 36816\r\n"]
[8.98642, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[8.995202, "o", "\u001b[2m2025-01-14T07:33:13.136349Z\u001b[0m \u001b[32m INFO\u001b[0m \u001b[2mvoidpin\u001b[0m\u001b[2m:\u001b[0m starting listener \u001b[3mgrpc\u001b[0m\u001b[2m=\u001b[0m\"0.0.0.0:7900\"\r\n"]
[9.060819, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[38;2;255;153;102mvoidpin\u001b[0m \u001b[90mHEAD\u001b[1;32m(153edb1)\u001b[0m \u001b[38;2;255;153;102m \u001b[0mis \u001b[1;38;5;208m📦 v0.0.1\u001b[0m \u001b[1;31mrs \u001b[0mwith \u001b[1;34m🐃 v1.49.0 \u001b[0m\r\n\u001b[1;34m✦\u001b[0m \u001b[38;2;255;153;102m\u001b[0m \u001b[K"]
[9.061906, "o", "\u001b[6 q"]
[9.062876, "o", "\u001b[6 q"]
[9.063056, "o", "\u001b[?2004h"]
[10.265829, "o", "s"]
[10.272815, "o", "\b\u001b[1m\u001b[31ms\u001b[0m\u001b[39m"]
[10.34456, "o", "\b\u001b[1m\u001b[31ms\u001b[0m\u001b[39m\u001b[90msh nef_remote\u001b[39m\u001b[13D"]
[10.409989, "o", "\b\u001b[1m\u001b[31ms\u001b[1m\u001b[31ms\u001b[0m\u001b[39m"]
[10.531573, "o", "\b\b\u001b[1m\u001b[31ms\u001b[1m\u001b[31ms\u001b[1m\u001b[31mh\u001b[0m\u001b[39m"]
[10.534938, "o", "\b\b\b\u001b[0m\u001b[32ms\u001b[0m\u001b[32ms\u001b[0m\u001b[32mh\u001b[39m"]
[10.650848, "o", "\b\u001b[32mh\u001b[32m \u001b[39m"]
[10.654699, "o", "\b\b\u001b[32mh\u001b[39m\u001b[39m "]
[12.060573, "o", "\u001b[39mn"]
[12.153753, "o", "\u001b[39me"]
[12.238632, "o", "\u001b[39mf"]
[12.536018, "o", "\u001b[39m_"]
[12.700909, "o", "\u001b[39mr"]
[12.776911, "o", "\u001b[39me"]
[12.839538, "o", "\u001b[39mm"]
[12.97043, "o", "\u001b[39mo"]
[13.030559, "o", "\u001b[39mt"]
[13.111063, "o", "\u001b[39me"]
[13.319003, "o", "\u001b[?1l\u001b>"]
[13.319486, "o", "\u001b[?2004l"]
[13.325988, "o", "\u001b[0 q"]
[13.326164, "o", "\r\r\n"]
[14.748903, "o", "Last login: Tue Jan 14 08:27:03 2025 from 10.0.9.20\r\r\n"]
[14.80584, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[14.840843, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1;33mkjuulh\u001b[0m in \u001b[1;2;32m🌐 nefarious\u001b[0m in \u001b[38;2;255;153;102m~\u001b[0m on \u001b[1;33m☁ (eu-west-1) \u001b[0m\r\n\u001b[38;2;255;153;102m\u001b[0m \u001b[K\u001b[6 q\u001b[6 q\u001b[?2004h"]
[15.45051, "o", "e"]
[15.456172, "o", "\b\u001b[1m\u001b[31me\u001b[0m\u001b[39m"]
[15.461708, "o", "\b\u001b[1m\u001b[31me\u001b[0m\u001b[39m\u001b[90mcho \"mystuff\" | voidpin remote copy\u001b[39m\u001b[35D"]
[15.623151, "o", "\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mx\u001b[0m\u001b[39m\u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[34D"]
[15.644024, "o", "\u001b[90mport VOIDPIN_REMOTE=http://10.0.9.20:7900\u001b[39m\u001b[41D"]
[15.743819, "o", "\b\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mx\u001b[1m\u001b[31mp\u001b[0m\u001b[39m"]
[15.908543, "o", "\b\u001b[1m\u001b[31mp\u001b[1m\u001b[31mo\u001b[0m\u001b[39m"]
[16.003031, "o", "\b\u001b[1m\u001b[31mo\u001b[1m\u001b[31mr\u001b[0m\u001b[39m"]
[16.265633, "o", "\b\u001b[1m\u001b[31mr\u001b[1m\u001b[31mt\u001b[0m\u001b[39m\b\b\b\b\b\b\u001b[0m\u001b[33me\u001b[0m\u001b[33mx\u001b[0m\u001b[33mp\u001b[0m\u001b[33mo\u001b[0m\u001b[33mr\u001b[0m\u001b[33mt\u001b[39m"]
[16.276286, "o", "\b\u001b[33mt\u001b[33m \u001b[39m\b\b\u001b[33mt\u001b[39m\u001b[39m "]
[17.117006, "o", "\u001b[39mV"]
[17.118576, "o", "\b\u001b[4mV\u001b[24m"]
[17.334112, "o", "\b\u001b[4mV\u001b[39m\u001b[4mO\u001b[24m\b\b\u001b[24mV\u001b[24mO"]
[17.363757, "o", "\u001b[39mI"]
[17.604581, "o", "\u001b[39mD"]
[17.931075, "o", "\u001b[39mP\u001b[39mI\u001b[39mN\u001b[39m_\u001b[39mR\u001b[39mE\u001b[39mM\u001b[39mO\u001b[39mT\u001b[39mE\u001b[39m=\u001b[39mh\u001b[39mt\u001b[39mt\u001b[39mp\u001b[39m:\u001b[39m/\u001b[39m/\u001b[39m1\u001b[39m0\u001b[39m.\u001b[39m0\u001b[39m.\u001b[39m9\u001b[39m.\u001b[39m2\u001b[39m0\u001b[39m:\u001b[39m7\u001b[39m9\u001b[39m0\u001b[39m0"]
[18.721578, "o", "\u001b[0 q\u001b[?2004l\r\r\n"]
[18.729848, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[18.745102, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1;33mkjuulh\u001b[0m in \u001b[1;2;32m🌐 nefarious\u001b[0m in \u001b[38;2;255;153;102m~\u001b[0m on \u001b[1;33m☁ (eu-west-1) \u001b[0m\r\n\u001b[38;2;255;153;102m\u001b[0m \u001b[K\u001b[6 q\u001b[6 q\u001b[?2004h"]
[19.768372, "o", "v\b\u001b[4mv\u001b[24m"]
[19.787679, "o", "\b\u001b[4mv\u001b[24m\u001b[90mirtualenv venv\u001b[39m\u001b[14D"]
[19.948701, "o", "\b\u001b[4mv\u001b[39m\u001b[4mo\u001b[24m\u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[13D"]
[19.950166, "o", "\b\b\u001b[24m\u001b[1m\u001b[31mv\u001b[24m\u001b[1m\u001b[31mo\u001b[0m\u001b[39m"]
[19.998976, "o", "\b\b\u001b[1m\u001b[31mv\u001b[1m\u001b[31mo\u001b[1m\u001b[31mi\u001b[0m\u001b[39m"]
[20.399333, "o", "\b\b\b\u001b[1m\u001b[31mv\u001b[1m\u001b[31mo\u001b[0m\u001b[39m\u001b[0m\u001b[39m \b"]
[20.536472, "o", "\b\b\u001b[1m\u001b[31mv\u001b[0m\u001b[39m\u001b[0m\u001b[39m \b\b\u001b[0m\u001b[39m\u001b[4mv\u001b[24m"]
[20.54648, "o", "\b\u001b[4mv\u001b[24m\u001b[90mirtualenv venv\u001b[39m\u001b[14D"]
[20.659047, "o", "\b\u001b[24m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[15D"]
[21.004154, "o", "e\b\u001b[1m\u001b[31me\u001b[0m\u001b[39m"]
[21.010045, "o", "\b\u001b[1m\u001b[31me\u001b[0m\u001b[39m\u001b[90mxport VOIDPIN_REMOTE=http://10.0.9.20:7900\u001b[39m\u001b[42D"]
[21.21325, "o", "\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mc\u001b[0m\u001b[39m\u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[41D"]
[21.222033, "o", "\u001b[90mho \"mystuff\" | voidpin remote copy\u001b[39m\u001b[34D"]
[21.294992, "o", "\b\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mc\u001b[1m\u001b[31mh\u001b[0m\u001b[39m"]
[21.419667, "o", "\b\u001b[1m\u001b[31mh\u001b[1m\u001b[31mo\u001b[0m\u001b[39m"]
[21.42291, "o", "\b\b\b\b\u001b[0m\u001b[32me\u001b[0m\u001b[32mc\u001b[0m\u001b[32mh\u001b[0m\u001b[32mo\u001b[39m"]
[21.505433, "o", "\b\u001b[32mo\u001b[32m \u001b[39m\b\b\u001b[32mo\u001b[39m\u001b[39m "]
[21.988846, "o", "\u001b[39m\"\u001b[39mm\u001b[39my\u001b[39ms\u001b[39mt\u001b[39mu\u001b[39mf\u001b[39mf\u001b[39m\"\u001b[39m \u001b[39m|\u001b[39m \u001b[39mv\u001b[39mo\u001b[39mi\u001b[39md\u001b[39mp\u001b[39mi\u001b[39mn\u001b[39m \u001b[39mr\u001b[39me\u001b[39mm\u001b[39mo\u001b[39mt\u001b[39me\u001b[39m \u001b[39mc\u001b[39mo\u001b[39mp\u001b[39my"]
[21.993557, "o", "\u001b[31D\u001b[33m\"\u001b[33mm\u001b[33my\u001b[33ms\u001b[33mt\u001b[33mu\u001b[33mf\u001b[33mf\u001b[33m\"\u001b[39m\u001b[3C\u001b[32mv\u001b[32mo\u001b[32mi\u001b[32md\u001b[32mp\u001b[32mi\u001b[32mn\u001b[39m\u001b[12C"]
[23.489379, "o", "\u001b[0 q"]
[23.490939, "o", "\u001b[?2004l\r\r\n"]
[23.569732, "o", "\u001b[2m2025-01-14T07:33:20.444640Z\u001b[0m \u001b[32m INFO\u001b[0m \u001b[2mvoidpin::remote_copy\u001b[0m\u001b[2m:\u001b[0m sending copy request\r\n"]
[23.595738, "o", "\u001b[2m2025-01-14T07:33:27.736730Z\u001b[0m \u001b[32m INFO\u001b[0m \u001b[2mvoidpin::copy\u001b[0m\u001b[2m:\u001b[0m copy process ended with status: ExitStatus(unix_wait_status(0))\n"]
[23.677319, "o", "\u001b[2m2025-01-14T07:33:20.549068Z\u001b[0m \u001b[32m INFO\u001b[0m \u001b[2mvoidpin::remote_copy\u001b[0m\u001b[2m:\u001b[0m sent copy request\r\n"]
[23.677877, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[23.682634, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[1;33mkjuulh\u001b[0m in \u001b[1;2;32m🌐 nefarious\u001b[0m in \u001b[38;2;255;153;102m~\u001b[0m on \u001b[1;33m☁ (eu-west-1) \u001b[0m\r\n\u001b[38;2;255;153;102m\u001b[0m \u001b[K"]
[23.684943, "o", "\u001b[6 q\u001b[6 q"]
[23.685144, "o", "\u001b[?2004h"]
[26.039094, "o", "e\b\u001b[1m\u001b[31me\u001b[0m\u001b[39m"]
[26.058901, "o", "\b\u001b[1m\u001b[31me\u001b[0m\u001b[39m\u001b[90mcho \"mystuff\" | voidpin remote copy\u001b[39m\u001b[35D"]
[26.200566, "o", "\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mx\u001b[0m\u001b[39m\u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[34D"]
[26.224342, "o", "\b\b\u001b[1m\u001b[31me\u001b[1m\u001b[31mx\u001b[1m\u001b[31mi\u001b[0m\u001b[39m"]
[26.236469, "o", "\u001b[90mt\u001b[39m\b"]
[26.338963, "o", "\b\u001b[1m\u001b[31mi\u001b[1m\u001b[31mt\u001b[0m\u001b[39m"]
[26.347595, "o", "\b\b\b\b\u001b[0m\u001b[32me\u001b[0m\u001b[32mx\u001b[0m\u001b[32mi\u001b[0m\u001b[32mt\u001b[39m"]
[27.917748, "o", "\u001b[0 q\u001b[?2004l\r\r\n"]
[27.92436, "o", "Connection to 10.0.9.18 closed.\r\r\n"]
[27.927279, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[28.005869, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[38;2;255;153;102mvoidpin\u001b[0m \u001b[90mHEAD\u001b[1;32m(153edb1)\u001b[0m \u001b[38;2;255;153;102m \u001b[0mis \u001b[1;38;5;208m📦 v0.0.1\u001b[0m \u001b[1;31mrs \u001b[0mwith \u001b[1;34m🐃 v1.49.0 \u001b[0m\u001b[33m14s\u001b[0m \r\n\u001b[1;34m✦\u001b[0m \u001b[38;2;255;153;102m\u001b[0m \u001b[K"]
[28.006941, "o", "\u001b[6 q"]
[28.007819, "o", "\u001b[6 q"]
[28.007998, "o", "\u001b[?2004h"]
[29.624611, "o", "p"]
[29.632533, "o", "\b\u001b[1m\u001b[31mp\u001b[0m\u001b[39m"]
[29.674006, "o", "\b\u001b[1m\u001b[31mp\u001b[0m\u001b[39m\u001b[90msql -h localhost -p 5432 -U postgres -d service\u001b[39m\u001b[47D"]
[29.861312, "o", "\b\u001b[1m\u001b[31mp\u001b[1m\u001b[31mb\u001b[0m\u001b[39m\u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[46D"]
[29.896875, "o", "\u001b[90mcopy\u001b[39m\b\b\b\b"]
[30.455748, "o", "\b\b\u001b[1m\u001b[31mp\u001b[1m\u001b[31mb\u001b[1m\u001b[31mp\u001b[0m\u001b[39m\u001b[39m \u001b[39m \u001b[39m \b\b\b"]
[30.492269, "o", "\u001b[90maste\u001b[39m\b\b\b\b"]
[30.511887, "o", "\b\u001b[1m\u001b[31mp\u001b[1m\u001b[31ma\u001b[0m\u001b[39m"]
[30.640428, "o", "\b\u001b[1m\u001b[31ma\u001b[1m\u001b[31ms\u001b[0m\u001b[39m"]
[30.900008, "o", "\b\u001b[1m\u001b[31ms\u001b[1m\u001b[31mt\u001b[0m\u001b[39m"]
[31.00196, "o", "\b\u001b[1m\u001b[31mt\u001b[1m\u001b[31me\u001b[0m\u001b[39m"]
[31.004152, "o", "\b\b\b\b\b\b\b\u001b[0m\u001b[32mp\u001b[0m\u001b[32mb\u001b[0m\u001b[32mp\u001b[0m\u001b[32ma\u001b[0m\u001b[32ms\u001b[0m\u001b[32mt\u001b[0m\u001b[32me\u001b[39m"]
[31.436893, "o", "\u001b[?1l\u001b>"]
[31.437147, "o", "\u001b[?2004l"]
[31.440483, "o", "\u001b[0 q"]
[31.440758, "o", "\r\r\n"]
[31.506519, "o", "mystuff\r\n"]
[31.507028, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[31.578124, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[38;2;255;153;102mvoidpin\u001b[0m \u001b[90mHEAD\u001b[1;32m(153edb1)\u001b[0m \u001b[38;2;255;153;102m \u001b[0mis \u001b[1;38;5;208m📦 v0.0.1\u001b[0m \u001b[1;31mrs \u001b[0mwith \u001b[1;34m🐃 v1.49.0 \u001b[0m\r\n\u001b[1;34m✦\u001b[0m \u001b[38;2;255;153;102m\u001b[0m \u001b[K"]
[31.579154, "o", "\u001b[6 q"]
[31.580021, "o", "\u001b[6 q"]
[31.580178, "o", "\u001b[?2004h"]
[33.751349, "o", "\u001b[?2004l\r\r\n"]
[33.75158, "o", "zsh: you have running jobs.\r\n"]
[33.751697, "o", "\u001b[1m\u001b[7m%\u001b[27m\u001b[1m\u001b[0m \r \r"]
[33.839059, "o", "\r\u001b[0m\u001b[27m\u001b[24m\u001b[J\r\n\u001b[38;2;255;153;102mvoidpin\u001b[0m \u001b[90mHEAD\u001b[1;32m(153edb1)\u001b[0m \u001b[38;2;255;153;102m \u001b[0mis \u001b[1;38;5;208m📦 v0.0.1\u001b[0m \u001b[1;31mrs \u001b[0mwith \u001b[1;34m🐃 v1.49.0 \u001b[0m\r\n\u001b[1;34m✦\u001b[0m \u001b[38;2;255;153;102m\u001b[0m \u001b[K"]
[33.840399, "o", "\u001b[6 q"]
[33.841412, "o", "\u001b[6 q"]
[33.84158, "o", "\u001b[?2004h"]
[39.131276, "o", "\u001b[?2004l\r\r\n"]
[39.13278, "o", "zsh: warning: 1 jobs SIGHUPed\r\n"]
[39.13415, "o", "[1] + hangup voidpin listen\r\n"]

BIN
assets/demo.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

10
buf.gen.yaml Normal file
View File

@ -0,0 +1,10 @@
version: v2
managed:
enabled: true
plugins:
- remote: buf.build/community/neoeinstein-prost
out: crates/voidpin/src/gen
- remote: buf.build/community/neoeinstein-tonic:v0.4.0
out: crates/voidpin/src/gen
inputs:
- directory: crates/voidpin/proto

4
buf.yaml Normal file
View File

@ -0,0 +1,4 @@
version: v2
modules:
- path: proto
name: buf.build/noschemaplz/voidpin

View File

@ -14,3 +14,8 @@ dotenv.workspace = true
serde = { version = "1.0.197", features = ["derive"] }
uuid = { version = "1.7.0", features = ["v4"] }
tonic = { version = "0.12.3", features = ["tls", "tls-roots"] }
prost = "0.13.4"
prost-types = "0.13.4"
bytes = "1.9.0"
async-trait = "0.1.85"

View File

@ -0,0 +1,13 @@
syntax = "proto3";
package voidpin.v1;
service VoidPin {
rpc Copy(CopyRequest) returns (CopyResponse);
}
message CopyRequest {
bytes content = 1;
}
message CopyResponse {}

View File

@ -0,0 +1,14 @@
// @generated
// This file is @generated by prost-build.
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct CopyRequest {
#[prost(bytes="vec", tag="1")]
pub content: ::prost::alloc::vec::Vec<u8>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, Copy, PartialEq, ::prost::Message)]
pub struct CopyResponse {
}
include!("voidpin.v1.tonic.rs");
// @@protoc_insertion_point(module)

View File

@ -0,0 +1,287 @@
// @generated
/// Generated client implementations.
pub mod void_pin_client {
#![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)]
use tonic::codegen::*;
use tonic::codegen::http::Uri;
///
#[derive(Debug, Clone)]
pub struct VoidPinClient<T> {
inner: tonic::client::Grpc<T>,
}
impl VoidPinClient<tonic::transport::Channel> {
/// Attempt to create a new client by connecting to a given endpoint.
pub async fn connect<D>(dst: D) -> Result<Self, tonic::transport::Error>
where
D: TryInto<tonic::transport::Endpoint>,
D::Error: Into<StdError>,
{
let conn = tonic::transport::Endpoint::new(dst)?.connect().await?;
Ok(Self::new(conn))
}
}
impl<T> VoidPinClient<T>
where
T: tonic::client::GrpcService<tonic::body::BoxBody>,
T::Error: Into<StdError>,
T::ResponseBody: Body<Data = Bytes> + Send + 'static,
<T::ResponseBody as Body>::Error: Into<StdError> + Send,
{
pub fn new(inner: T) -> Self {
let inner = tonic::client::Grpc::new(inner);
Self { inner }
}
pub fn with_origin(inner: T, origin: Uri) -> Self {
let inner = tonic::client::Grpc::with_origin(inner, origin);
Self { inner }
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> VoidPinClient<InterceptedService<T, F>>
where
F: tonic::service::Interceptor,
T::ResponseBody: Default,
T: tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
Response = http::Response<
<T as tonic::client::GrpcService<tonic::body::BoxBody>>::ResponseBody,
>,
>,
<T as tonic::codegen::Service<
http::Request<tonic::body::BoxBody>,
>>::Error: Into<StdError> + Send + Sync,
{
VoidPinClient::new(InterceptedService::new(inner, interceptor))
}
/// Compress requests with the given encoding.
///
/// This requires the server to support it otherwise it might respond with an
/// error.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.send_compressed(encoding);
self
}
/// Enable decompressing responses.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.inner = self.inner.accept_compressed(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_decoding_message_size(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.inner = self.inner.max_encoding_message_size(limit);
self
}
///
pub async fn copy(
&mut self,
request: impl tonic::IntoRequest<super::CopyRequest>,
) -> std::result::Result<tonic::Response<super::CopyResponse>, tonic::Status> {
self.inner
.ready()
.await
.map_err(|e| {
tonic::Status::new(
tonic::Code::Unknown,
format!("Service was not ready: {}", e.into()),
)
})?;
let codec = tonic::codec::ProstCodec::default();
let path = http::uri::PathAndQuery::from_static("/voidpin.v1.VoidPin/Copy");
let mut req = request.into_request();
req.extensions_mut().insert(GrpcMethod::new("voidpin.v1.VoidPin", "Copy"));
self.inner.unary(req, path, codec).await
}
}
}
/// Generated server implementations.
pub mod void_pin_server {
#![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)]
use tonic::codegen::*;
/// Generated trait containing gRPC methods that should be implemented for use with VoidPinServer.
#[async_trait]
pub trait VoidPin: Send + Sync + 'static {
///
async fn copy(
&self,
request: tonic::Request<super::CopyRequest>,
) -> std::result::Result<tonic::Response<super::CopyResponse>, tonic::Status>;
}
///
#[derive(Debug)]
pub struct VoidPinServer<T: VoidPin> {
inner: _Inner<T>,
accept_compression_encodings: EnabledCompressionEncodings,
send_compression_encodings: EnabledCompressionEncodings,
max_decoding_message_size: Option<usize>,
max_encoding_message_size: Option<usize>,
}
struct _Inner<T>(Arc<T>);
impl<T: VoidPin> VoidPinServer<T> {
pub fn new(inner: T) -> Self {
Self::from_arc(Arc::new(inner))
}
pub fn from_arc(inner: Arc<T>) -> Self {
let inner = _Inner(inner);
Self {
inner,
accept_compression_encodings: Default::default(),
send_compression_encodings: Default::default(),
max_decoding_message_size: None,
max_encoding_message_size: None,
}
}
pub fn with_interceptor<F>(
inner: T,
interceptor: F,
) -> InterceptedService<Self, F>
where
F: tonic::service::Interceptor,
{
InterceptedService::new(Self::new(inner), interceptor)
}
/// Enable decompressing requests with the given encoding.
#[must_use]
pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.accept_compression_encodings.enable(encoding);
self
}
/// Compress responses with the given encoding, if the client supports it.
#[must_use]
pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self {
self.send_compression_encodings.enable(encoding);
self
}
/// Limits the maximum size of a decoded message.
///
/// Default: `4MB`
#[must_use]
pub fn max_decoding_message_size(mut self, limit: usize) -> Self {
self.max_decoding_message_size = Some(limit);
self
}
/// Limits the maximum size of an encoded message.
///
/// Default: `usize::MAX`
#[must_use]
pub fn max_encoding_message_size(mut self, limit: usize) -> Self {
self.max_encoding_message_size = Some(limit);
self
}
}
impl<T, B> tonic::codegen::Service<http::Request<B>> for VoidPinServer<T>
where
T: VoidPin,
B: Body + Send + 'static,
B::Error: Into<StdError> + Send + 'static,
{
type Response = http::Response<tonic::body::BoxBody>;
type Error = std::convert::Infallible;
type Future = BoxFuture<Self::Response, Self::Error>;
fn poll_ready(
&mut self,
_cx: &mut Context<'_>,
) -> Poll<std::result::Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: http::Request<B>) -> Self::Future {
let inner = self.inner.clone();
match req.uri().path() {
"/voidpin.v1.VoidPin/Copy" => {
#[allow(non_camel_case_types)]
struct CopySvc<T: VoidPin>(pub Arc<T>);
impl<T: VoidPin> tonic::server::UnaryService<super::CopyRequest>
for CopySvc<T> {
type Response = super::CopyResponse;
type Future = BoxFuture<
tonic::Response<Self::Response>,
tonic::Status,
>;
fn call(
&mut self,
request: tonic::Request<super::CopyRequest>,
) -> Self::Future {
let inner = Arc::clone(&self.0);
let fut = async move {
<T as VoidPin>::copy(&inner, request).await
};
Box::pin(fut)
}
}
let accept_compression_encodings = self.accept_compression_encodings;
let send_compression_encodings = self.send_compression_encodings;
let max_decoding_message_size = self.max_decoding_message_size;
let max_encoding_message_size = self.max_encoding_message_size;
let inner = self.inner.clone();
let fut = async move {
let inner = inner.0;
let method = CopySvc(inner);
let codec = tonic::codec::ProstCodec::default();
let mut grpc = tonic::server::Grpc::new(codec)
.apply_compression_config(
accept_compression_encodings,
send_compression_encodings,
)
.apply_max_message_size_config(
max_decoding_message_size,
max_encoding_message_size,
);
let res = grpc.unary(method, req).await;
Ok(res)
};
Box::pin(fut)
}
_ => {
Box::pin(async move {
Ok(
http::Response::builder()
.status(200)
.header("grpc-status", "12")
.header("content-type", "application/grpc")
.body(empty_body())
.unwrap(),
)
})
}
}
}
}
impl<T: VoidPin> Clone for VoidPinServer<T> {
fn clone(&self) -> Self {
let inner = self.inner.clone();
Self {
inner,
accept_compression_encodings: self.accept_compression_encodings,
send_compression_encodings: self.send_compression_encodings,
max_decoding_message_size: self.max_decoding_message_size,
max_encoding_message_size: self.max_encoding_message_size,
}
}
}
impl<T: VoidPin> Clone for _Inner<T> {
fn clone(&self) -> Self {
Self(Arc::clone(&self.0))
}
}
impl<T: std::fmt::Debug> std::fmt::Debug for _Inner<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl<T: VoidPin> tonic::server::NamedService for VoidPinServer<T> {
const NAME: &'static str = "voidpin.v1.VoidPin";
}
}

View File

@ -0,0 +1,30 @@
use crate::{copy::LocalCopierState, state::State};
#[derive(Clone)]
pub struct GrpcServer {
state: State,
}
impl GrpcServer {
pub fn new(state: State) -> Self {
Self { state }
}
}
#[async_trait::async_trait]
impl crate::grpc::void_pin_server::VoidPin for GrpcServer {
async fn copy(
&self,
req: tonic::Request<crate::grpc::CopyRequest>,
) -> Result<tonic::Response<crate::grpc::CopyResponse>, tonic::Status> {
let req = req.into_inner();
self.state
.local_copier()
.copy(&req.content)
.await
.map_err(|e| tonic::Status::internal(e.to_string()))?;
Ok(tonic::Response::new(crate::grpc::CopyResponse {}))
}
}

View File

@ -1,11 +1,22 @@
use std::io::Read;
use std::{io::Read, net::SocketAddr};
use anyhow::Context;
use clap::{Parser, Subcommand};
use copy::LocalCopierState;
use grpc::void_pin_server::VoidPinServer;
use grpc_server::GrpcServer;
use remote_copy::RemoteCopierState;
use state::State;
use tonic::transport;
mod grpc {
include!("gen/voidpin.v1.rs");
}
mod grpc_server;
mod copy;
mod remote_copy;
mod state;
#[derive(Parser)]
@ -17,7 +28,10 @@ struct Command {
#[derive(Subcommand)]
enum Commands {
Listen {},
Listen {
#[arg(long, env = "VOIDPIN_GRPC_HOST", default_value = "0.0.0.0:7900")]
grpc: SocketAddr,
},
Copy {},
Remote {
#[command(subcommand)]
@ -27,7 +41,14 @@ enum Commands {
#[derive(Subcommand)]
enum RemoteCommands {
Copy {},
Copy {
#[arg(
long = "remote-host",
env = "VOIDPIN_REMOTE",
default_value = "http://0.0.0.0:7900"
)]
remote_host: String,
},
}
#[tokio::main]
@ -41,7 +62,13 @@ async fn main() -> anyhow::Result<()> {
let state = State::new();
match cli.command.unwrap() {
Commands::Listen {} => {}
Commands::Listen { grpc } => {
tracing::info!(grpc = grpc.to_string(), "starting listener");
transport::Server::builder()
.add_service(VoidPinServer::new(GrpcServer::new(state)))
.serve(grpc)
.await?;
}
Commands::Copy {} => {
let mut input = String::new();
std::io::stdin()
@ -57,7 +84,23 @@ async fn main() -> anyhow::Result<()> {
state.local_copier().copy(input.as_bytes()).await?;
}
Commands::Remote { command } => match command {
RemoteCommands::Copy {} => todo!(),
RemoteCommands::Copy { remote_host } => {
let mut input = String::new();
std::io::stdin()
.read_to_string(&mut input)
.context("failed to read from stdin")?;
if input.is_empty() {
tracing::info!("no content to put in clipboard");
return Ok(());
}
tracing::debug!(content = &input, "found content");
state
.remote_copier(&remote_host)
.copy(input.as_bytes())
.await?;
}
},
_ => (),
}

View File

@ -0,0 +1,50 @@
use tonic::transport::{Channel, ClientTlsConfig};
use crate::{grpc::CopyRequest, state::State};
#[derive(Default)]
pub struct RemoteCopier {
host: String,
}
impl RemoteCopier {
pub fn new(remote_host: impl Into<String>) -> Self {
Self {
host: remote_host.into(),
}
}
pub async fn copy(&self, input: &[u8]) -> anyhow::Result<()> {
let tls = ClientTlsConfig::new();
let channel = Channel::from_shared(self.host.clone())?
.tls_config(if self.host.starts_with("https") {
tls.with_native_roots()
} else {
tls
})?
.connect()
.await?;
tracing::debug!("establishing connection to remote");
let mut client = crate::grpc::void_pin_client::VoidPinClient::new(channel);
tracing::info!("sending copy request");
client
.copy(CopyRequest {
content: input.into(),
})
.await?;
tracing::info!("sent copy request");
Ok(())
}
}
pub trait RemoteCopierState {
fn remote_copier(&self, host: impl Into<String>) -> RemoteCopier;
}
impl RemoteCopierState for State {
fn remote_copier(&self, host: impl Into<String>) -> RemoteCopier {
RemoteCopier::new(host)
}
}