├── .envrc ├── .gitignore ├── CHANGELOG.adoc ├── Makefile ├── README.adoc ├── UNLICENSE ├── default.nix ├── derivation.nix ├── osxsnarf.c └── overlay.nix /.envrc: -------------------------------------------------------------------------------- 1 | use nix 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /result 2 | /osxsnarf 3 | *.o 4 | -------------------------------------------------------------------------------- /CHANGELOG.adoc: -------------------------------------------------------------------------------- 1 | CHANGELOG 2 | ========= 3 | 4 | https://github.com/eraserhd/rep/compare/v0.1.0...v0.1.1[Unreleased] 5 | ----------------------------------------------------------------- 6 | 7 | 8 | v0.1.0 - 2019-09-17 9 | ------------------- 10 | 11 | * Add Makefile 12 | * Add UNLICENSE 13 | * `-f` option processes in foreground. 14 | * Store and retrieve utf8 text (don't translate). Fixes garbage when run on 15 | Mac OS X 10.14.4 16 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | prefix=/usr/local 3 | 4 | all: osxsnarf 5 | 6 | osxsnarf: osxsnarf.o 7 | 9 9l -o osxsnarf osxsnarf.o 8 | 9 | osxsnarf.o: osxsnarf.c 10 | 9 9c osxsnarf.c 11 | 12 | install: osxsnarf 13 | mkdir -p $(prefix)/bin 14 | cp osxsnarf $(prefix)/bin/ 15 | 16 | .PHONY: all install 17 | -------------------------------------------------------------------------------- /README.adoc: -------------------------------------------------------------------------------- 1 | osxsnarf 2 | ======== 3 | 4 | A Plan 9-inspired way to share your OS X clipboard to multiple hosts. 5 | 6 | This was taken from http://mirtchovski.com/p9/osxsnarf/[mirtchovski.com]. 7 | 8 | Installation 9 | ------------ 10 | 11 | Assuming https://9fans.github.io/plan9port/[plan9port] is installed, and `9` 12 | is in the path, you can build with the following commands: 13 | 14 | [source,sh] 15 | ---- 16 | $ make 17 | $ sudo make prefix=/usr/local install 18 | ---- 19 | 20 | Running and Usage 21 | ----------------- 22 | 23 | You can have it listen on a Unix socket and post as a service where plan9port 24 | might expect to find it. 25 | 26 | [source,sh] 27 | ---- 28 | $ 9 ./osxsnarf 'unix!'"$(9 namespace)/snarf" 29 | $ 9 9p ls snarf 30 | snarf 31 | $ printf 'hello, world!\n' |9 9p write snarf/snarf 32 | $ pbpaste 33 | hello, world! 34 | $ 9 9p read snarf/snarf 35 | hello, world! 36 | $ printf 'goodbye, world!\n' |pbcopy 37 | $ 9 9p read snarf/snarf 38 | goodbye, world! 39 | ---- 40 | 41 | The weird quoting is to avoid Mac OS's default bash from interpreting the 42 | `!`. 43 | 44 | Forwarding the Snarf Service 45 | ---------------------------- 46 | 47 | You can share the service with other machines using `9 import` or `autossh`. 48 | An example how to do this with `autossh`: 49 | 50 | [source,sh] 51 | ---- 52 | autossh -M 0 -f \ 53 | -o 'StreamLocalBindUnlink yes' \ 54 | -o 'ServerAliveInterval 30' \ 55 | -o 'ServerAliveCountMax 3' \ 56 | -o 'ExitOnForwardFailure yes' \ 57 | -R"/path/to/remote/namespace/snarf:$(9 namespace)/snarf" \ 58 | -T -N crunch.eraserhead.net 59 | ---- 60 | 61 | While this is running, both systems will have access to the OSX pasteboard. 62 | 63 | `StreamLocalBindUnlink` instructs ssh to remove the forwarded unix socket on 64 | disconnect. Without this, if the connection is broken it will fail to 65 | re-establish, since ssh will not recreate the socket. 66 | 67 | Configuring Kakoune 68 | ------------------- 69 | 70 | The following mappings are useful in Kakoune to interact with the system 71 | pasteboard via the "snarf" service: 72 | 73 | ---- 74 | # System clipboard handling 75 | # ───────────────────────── 76 | map global user -docstring 'paste (after) from clipboard' p '!9 9p read snarf/snarf' 77 | map global user -docstring 'paste (before) from clipboard' P '9 9p read snarf/snarf' 78 | map global user -docstring 'yank to clipboard' y '9 9p write snarf/snarf:echo -markup %{{Information}copied selection to system clipboard}' 79 | map global user -docstring 'replace from clipboard' R '|9 9p read snarf/snarf' 80 | ---- 81 | 82 | Bugs 83 | ---- 84 | 85 | * There's a fixed buffer size for text, about 192Kb. This should be dynamically allocated. 86 | -------------------------------------------------------------------------------- /UNLICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | let 2 | pkgs = import { 3 | config = {}; 4 | overlays = [ 5 | (import ./overlay.nix) 6 | ]; 7 | }; 8 | in pkgs.osxsnarf 9 | -------------------------------------------------------------------------------- /derivation.nix: -------------------------------------------------------------------------------- 1 | { stdenv, lib, plan9port, darwin, ... }: 2 | 3 | stdenv.mkDerivation { 4 | pname = "osxsnarf"; 5 | version = "0.1.0"; 6 | src = ./.; 7 | 8 | buildInputs = [ plan9port darwin.apple_sdk.frameworks.Carbon ]; 9 | makeFlags = [ "prefix=${placeholder "out"}" ]; 10 | 11 | meta = with lib; { 12 | description = "A Plan 9-inspired way to share your OS X clipboard."; 13 | homepage = https://github.com/eraserhd/osxsnarf; 14 | license = licenses.unlicense; 15 | platforms = platforms.darwin; 16 | maintainers = [ maintainers.eraserhd ]; 17 | }; 18 | } 19 | -------------------------------------------------------------------------------- /osxsnarf.c: -------------------------------------------------------------------------------- 1 | #undef nil 2 | 3 | #include 4 | #include 5 | AUTOFRAMEWORK(Carbon) 6 | #include 7 | #include 8 | #include 9 | #include <9p.h> 10 | 11 | static void fsread(Req *); 12 | static void fswrite(Req *); 13 | 14 | enum { 15 | Qroot = 0, 16 | Qsnarf, 17 | SnarfSize = 3*64*1024, 18 | }; 19 | 20 | Srv fs= 21 | { 22 | .read = fsread, 23 | .write = fswrite, 24 | }; 25 | 26 | static PasteboardRef appleclip; 27 | 28 | static char snarf[SnarfSize+1]; 29 | 30 | static char *deflisten = "tcp!*!18001"; 31 | 32 | RWLock l; 33 | 34 | void 35 | usage(void) 36 | { 37 | fprint(2, "usage: %s [-f] [-D] [listen]\n", argv0); 38 | sysfatal("usage"); 39 | } 40 | 41 | void 42 | fswrite(Req *r) 43 | { 44 | CFDataRef cfdata; 45 | PasteboardSyncFlags flags; 46 | char err[128]; 47 | 48 | if((int)r->fid->file->aux != Qsnarf) { 49 | respond(r, "no such file or directory"); 50 | return; 51 | } 52 | 53 | if(r->ifcall.offset >= SnarfSize) { 54 | sprintf(err, "writing too much for this buffer. max size is: %d", SnarfSize); 55 | respond(r, err); 56 | return; 57 | } 58 | 59 | wlock(&l); 60 | /* silently truncate here. perhaps better to return an error? */ 61 | if(r->ifcall.offset + r->ifcall.count > SnarfSize) 62 | r->ifcall.count = SnarfSize - r->ifcall.offset; 63 | 64 | memmove(snarf+r->ifcall.offset, r->ifcall.data, r->ifcall.count); 65 | snarf[r->ifcall.offset+r->ifcall.count] = '\0'; 66 | 67 | if(PasteboardClear(appleclip) != noErr){ 68 | respond(r, "apple pasteboard clear failed"); 69 | goto werr; 70 | } 71 | flags = PasteboardSynchronize(appleclip); 72 | if((flags&kPasteboardModified) || !(flags&kPasteboardClientIsOwner)){ 73 | respond(r, "apple pasteboard cannot assert ownership"); 74 | goto werr; 75 | } 76 | cfdata = CFDataCreate(kCFAllocatorDefault, (uint8_t*)snarf, strlen(snarf)); 77 | 78 | if(cfdata == nil){ 79 | respond(r, "apple pasteboard cfdatacreate failed"); 80 | goto werr; 81 | } 82 | if(PasteboardPutItemFlavor(appleclip, (PasteboardItemID)1, 83 | CFSTR("public.utf8-plain-text"), cfdata, 0) != noErr){ 84 | respond(r, "apple pasteboard putitem failed"); 85 | CFRelease(cfdata); 86 | goto werr; 87 | } 88 | CFRelease(cfdata); 89 | r->ofcall.count = r->ifcall.count; 90 | respond(r, nil); 91 | 92 | werr: 93 | wunlock(&l); 94 | return; 95 | } 96 | 97 | void 98 | fsread(Req *r) 99 | { 100 | CFDataRef cfdata; 101 | OSStatus err = noErr; 102 | ItemCount nItems; 103 | uint32_t i; 104 | 105 | if((int)r->fid->file->aux != Qsnarf) { 106 | respond(r, "no such file or directory"); 107 | return; 108 | } 109 | 110 | rlock(&l); 111 | 112 | PasteboardSynchronize(appleclip); 113 | if((err = PasteboardGetItemCount(appleclip, &nItems)) != noErr) { 114 | respond(r, "apple pasteboard GetItemCount failed"); 115 | goto rerr; 116 | } 117 | 118 | for(i = 1; i <= nItems; ++i) { 119 | PasteboardItemID itemID; 120 | CFArrayRef flavorTypeArray; 121 | CFIndex flavorCount; 122 | 123 | if((err = PasteboardGetItemIdentifier(appleclip, i, &itemID)) != noErr){ 124 | respond(r, "can't get pasteboard item identifier"); 125 | goto rerr; 126 | } 127 | 128 | if((err = PasteboardCopyItemFlavors(appleclip, itemID, &flavorTypeArray))!=noErr){ 129 | respond(r, "Can't copy pasteboard item flavors"); 130 | goto rerr; 131 | } 132 | 133 | flavorCount = CFArrayGetCount(flavorTypeArray); 134 | CFIndex flavorIndex; 135 | for(flavorIndex = 0; flavorIndex < flavorCount; ++flavorIndex){ 136 | CFStringRef flavorType; 137 | flavorType = (CFStringRef)CFArrayGetValueAtIndex(flavorTypeArray, flavorIndex); 138 | if (UTTypeConformsTo(flavorType, CFSTR("public.utf8-plain-text"))){ 139 | if((err = PasteboardCopyItemFlavorData(appleclip, itemID, 140 | CFSTR("public.utf8-plain-text"), &cfdata)) != noErr){ 141 | respond(r, "apple pasteboard CopyItem failed"); 142 | goto rerr; 143 | } 144 | CFIndex length = CFDataGetLength(cfdata); 145 | if (length > sizeof snarf - 1) length = sizeof snarf - 1; 146 | CFDataGetBytes(cfdata, CFRangeMake(0, length), (uint8_t *)snarf); 147 | snarf[length] = '\0'; 148 | char *s = snarf; 149 | while (*s) { 150 | if (*s == '\r') *s = '\n'; 151 | s++; 152 | } 153 | CFRelease(cfdata); 154 | } 155 | } 156 | } 157 | 158 | readstr(r, snarf); 159 | respond(r, nil); 160 | 161 | rerr: 162 | runlock(&l); 163 | } 164 | 165 | void 166 | threadmain(int argc, char **argv) 167 | { 168 | File *rootf; 169 | char *lstn = deflisten; 170 | 171 | ARGBEGIN{ 172 | case 'f': 173 | fs.foreground = 1; 174 | break; 175 | case 'D': 176 | chatty9p++; 177 | break; 178 | break; 179 | default: 180 | usage(); 181 | }ARGEND 182 | if(argc == 1) 183 | lstn = argv[0]; 184 | else if(argc > 1) 185 | usage(); 186 | 187 | fs.tree = alloctree(getuser(), getuser(), DMDIR|0555, nil); 188 | rootf = createfile(fs.tree->root, "snarf", getuser(), 0666, nil); 189 | if(rootf == nil) 190 | sysfatal("creating snarf: %r"); 191 | rootf->aux = (void *)Qsnarf; 192 | 193 | if(PasteboardCreate(kPasteboardClipboard, &appleclip) != noErr) 194 | sysfatal("pasteboard create failed"); 195 | 196 | threadpostmountsrv(&fs, lstn, nil, MREPL|MCREATE); 197 | 198 | threadexits(nil); 199 | } 200 | -------------------------------------------------------------------------------- /overlay.nix: -------------------------------------------------------------------------------- 1 | self: super: { 2 | osxsnarf = super.callPackage ./derivation.nix {}; 3 | } 4 | --------------------------------------------------------------------------------