quickly upload files to a remote server via rsync
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add clipboard support and clean up code

eti 2935127e af99ec0e

+149 -112
+99
bun.lock
··· 4 4 "": { 5 5 "name": "hop", 6 6 "dependencies": { 7 + "clipboardy": "^5.3.0", 7 8 "ora": "^8.0.0", 8 9 }, 9 10 "devDependencies": { ··· 15 16 }, 16 17 }, 17 18 "packages": { 19 + "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], 20 + 21 + "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], 22 + 18 23 "@types/bun": ["@types/bun@1.3.8", "", { "dependencies": { "bun-types": "1.3.8" } }, "sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA=="], 19 24 20 25 "@types/node": ["@types/node@25.2.0", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w=="], ··· 28 33 "cli-cursor": ["cli-cursor@5.0.0", "", { "dependencies": { "restore-cursor": "^5.0.0" } }, "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw=="], 29 34 30 35 "cli-spinners": ["cli-spinners@2.9.2", "", {}, "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg=="], 36 + 37 + "clipboard-image": ["clipboard-image@0.1.0", "", { "dependencies": { "run-jxa": "^3.0.0" }, "bin": { "clipboard-image": "cli.js" } }, "sha512-SWk7FgaXLNFld19peQ/rTe0n97lwR1WbkqxV6JKCAOh7U52AKV/PeMFCyt/8IhBdqyDA8rdyewQMKZqvWT5Akg=="], 38 + 39 + "clipboardy": ["clipboardy@5.3.0", "", { "dependencies": { "clipboard-image": "^0.1.0", "execa": "^9.6.1", "is-wayland": "^0.1.0", "is-wsl": "^3.1.0", "is64bit": "^2.0.0", "powershell-utils": "^0.2.0" } }, "sha512-EOei1RJTbqXbXhUBMGN8C/Pf+QIPrnDWx9ztlmcW5Hljqj/oVlPrlrDw2O4xh5ViHcvHX3+A0zBrCdcptKTaJA=="], 40 + 41 + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], 42 + 43 + "crypto-random-string": ["crypto-random-string@4.0.0", "", { "dependencies": { "type-fest": "^1.0.1" } }, "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA=="], 31 44 32 45 "emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="], 33 46 47 + "escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], 48 + 49 + "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], 50 + 51 + "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], 52 + 34 53 "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], 35 54 55 + "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], 56 + 57 + "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], 58 + 59 + "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], 60 + 61 + "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], 62 + 36 63 "is-interactive": ["is-interactive@2.0.0", "", {}, "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ=="], 37 64 65 + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], 66 + 67 + "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], 68 + 38 69 "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], 39 70 71 + "is-wayland": ["is-wayland@0.1.0", "", {}, "sha512-QkbMsWkIfkrzOPxenwye0h56iAXirZYHG9eHVPb22fO9y+wPbaX/CHacOWBa/I++4ohTcByimhM1/nyCsH8KNA=="], 72 + 73 + "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], 74 + 75 + "is64bit": ["is64bit@2.0.0", "", { "dependencies": { "system-architecture": "^0.1.0" } }, "sha512-jv+8jaWCl0g2lSBkNSVXdzfBA0npK1HGC2KtWM9FumFRoGS94g3NbCCLVnCYHLjp4GrW2KZeeSTMo5ddtznmGw=="], 76 + 77 + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], 78 + 40 79 "log-symbols": ["log-symbols@6.0.0", "", { "dependencies": { "chalk": "^5.3.0", "is-unicode-supported": "^1.3.0" } }, "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw=="], 41 80 81 + "macos-version": ["macos-version@6.0.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-O2S8voA+pMfCHhBn/TIYDXzJ1qNHpPDU32oFxglKnVdJABiYYITt45oLkV9yhwA3E2FDwn3tQqUFrTsr1p3sBQ=="], 82 + 83 + "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], 84 + 85 + "mimic-fn": ["mimic-fn@2.1.0", "", {}, "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="], 86 + 42 87 "mimic-function": ["mimic-function@5.0.1", "", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="], 88 + 89 + "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], 43 90 44 91 "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], 45 92 46 93 "ora": ["ora@8.2.0", "", { "dependencies": { "chalk": "^5.3.0", "cli-cursor": "^5.0.0", "cli-spinners": "^2.9.2", "is-interactive": "^2.0.0", "is-unicode-supported": "^2.0.0", "log-symbols": "^6.0.0", "stdin-discarder": "^0.2.2", "string-width": "^7.2.0", "strip-ansi": "^7.1.0" } }, "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw=="], 47 94 95 + "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], 96 + 97 + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], 98 + 99 + "powershell-utils": ["powershell-utils@0.2.0", "", {}, "sha512-ZlsFlG7MtSFCoc5xreOvBAozCJ6Pf06opgJjh9ONEv418xpZSAzNjstD36C6+JwOnfSqOW/9uDkqKjezTdxZhw=="], 100 + 101 + "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], 102 + 48 103 "restore-cursor": ["restore-cursor@5.1.0", "", { "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" } }, "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA=="], 104 + 105 + "run-jxa": ["run-jxa@3.0.0", "", { "dependencies": { "execa": "^5.1.1", "macos-version": "^6.0.0", "subsume": "^4.0.0", "type-fest": "^2.0.0" } }, "sha512-4f2CrY7H+sXkKXJn/cE6qRA3z+NMVO7zvlZ/nUV0e62yWftpiLAfw5eV9ZdomzWd2TXWwEIiGjAT57+lWIzzvA=="], 106 + 107 + "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], 108 + 109 + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], 110 + 111 + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], 49 112 50 113 "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], 51 114 ··· 55 118 56 119 "strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], 57 120 121 + "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], 122 + 123 + "subsume": ["subsume@4.0.0", "", { "dependencies": { "escape-string-regexp": "^5.0.0", "unique-string": "^3.0.0" } }, "sha512-BWnYJElmHbYZ/zKevy+TG+SsyoFCmRPDHJbR1MzLxkPOv1Jp/4hGhVUtP98s+wZBsBsHwCXvPTP0x287/WMjGg=="], 124 + 125 + "system-architecture": ["system-architecture@0.1.0", "", {}, "sha512-ulAk51I9UVUyJgxlv9M6lFot2WP3e7t8Kz9+IS6D4rVba1tR9kON+Ey69f+1R4Q8cd45Lod6a4IcJIxnzGc/zA=="], 126 + 127 + "type-fest": ["type-fest@2.19.0", "", {}, "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA=="], 128 + 58 129 "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], 59 130 60 131 "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], 61 132 133 + "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], 134 + 135 + "unique-string": ["unique-string@3.0.0", "", { "dependencies": { "crypto-random-string": "^4.0.0" } }, "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ=="], 136 + 137 + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], 138 + 139 + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], 140 + 141 + "crypto-random-string/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], 142 + 62 143 "log-symbols/is-unicode-supported": ["is-unicode-supported@1.3.0", "", {}, "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ=="], 144 + 145 + "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], 146 + 147 + "run-jxa/execa": ["execa@5.1.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", "human-signals": "^2.1.0", "is-stream": "^2.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^4.0.1", "onetime": "^5.1.2", "signal-exit": "^3.0.3", "strip-final-newline": "^2.0.0" } }, "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg=="], 148 + 149 + "run-jxa/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="], 150 + 151 + "run-jxa/execa/human-signals": ["human-signals@2.1.0", "", {}, "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw=="], 152 + 153 + "run-jxa/execa/is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], 154 + 155 + "run-jxa/execa/npm-run-path": ["npm-run-path@4.0.1", "", { "dependencies": { "path-key": "^3.0.0" } }, "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw=="], 156 + 157 + "run-jxa/execa/onetime": ["onetime@5.1.2", "", { "dependencies": { "mimic-fn": "^2.1.0" } }, "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg=="], 158 + 159 + "run-jxa/execa/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], 160 + 161 + "run-jxa/execa/strip-final-newline": ["strip-final-newline@2.0.0", "", {}, "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA=="], 63 162 } 64 163 }
+1
package.json
··· 10 10 "test": "bun test" 11 11 }, 12 12 "dependencies": { 13 + "clipboardy": "^5.3.0", 13 14 "ora": "^8.0.0" 14 15 }, 15 16 "devDependencies": {
+34 -32
src/clipboard.ts
··· 1 - import { spawn } from "child_process"; 2 - import { PBCOPY_TIMEOUT_MS } from "./constants"; 1 + import { writeFile, unlink } from "fs/promises"; 2 + import clipboard from "clipboardy"; 3 3 4 - /** 5 - * Copies text to the system clipboard using pbcopy (macOS) 6 - * @param text The text to copy to clipboard 7 - * @returns Promise that resolves when copy is complete 8 - * @throws Error if pbcopy fails or times out 9 - */ 10 - export async function copyToClipboard(text: string): Promise<void> { 11 - return new Promise((resolve, reject) => { 12 - const pbcopy = spawn("pbcopy"); 13 - let timeoutId: Timer | null = null; 4 + export interface ClipboardContent { 5 + type: "image" | "text"; 6 + path: string; 7 + } 14 8 15 - pbcopy.stdin.write(text); 16 - pbcopy.stdin.end(); 9 + export async function getClipboardContent(): Promise<ClipboardContent | null> { 10 + // Check for images first 11 + const hasImages = await clipboard.hasImages(); 12 + if (hasImages) { 13 + const imagePaths = await clipboard.readImages(); 14 + if (imagePaths.length > 0) { 15 + return { type: "image", path: imagePaths[0] }; 16 + } 17 + } 17 18 18 - pbcopy.on("close", (code) => { 19 - if (timeoutId) clearTimeout(timeoutId); 20 - if (code === 0) { 21 - resolve(); 22 - } else { 23 - reject(new Error(`pbcopy exited with code ${code}`)); 24 - } 25 - }); 19 + // Fall back to text 20 + const text = await clipboard.read(); 21 + if (text && text.length > 0) { 22 + const tempPath = `/tmp/hop-clipboard-${Date.now()}`; 23 + await writeFile(tempPath, text); 24 + return { type: "text", path: tempPath }; 25 + } 26 + 27 + return null; 28 + } 26 29 27 - pbcopy.on("error", (err) => { 28 - if (timeoutId) clearTimeout(timeoutId); 29 - reject(err); 30 - }); 30 + export async function cleanupClipboardFile(path: string): Promise<void> { 31 + try { 32 + await unlink(path); 33 + } catch { 34 + // Ignore cleanup errors 35 + } 36 + } 31 37 32 - // Add timeout for clipboard operation 33 - timeoutId = setTimeout(() => { 34 - pbcopy.kill("SIGTERM"); 35 - reject(new Error("pbcopy timed out")); 36 - }, PBCOPY_TIMEOUT_MS); 37 - }); 38 + export async function copyToClipboard(text: string): Promise<void> { 39 + await clipboard.write(text); 38 40 }
-12
src/config.ts
··· 3 3 import { homedir } from "os"; 4 4 import { join } from "path"; 5 5 6 - /** 7 - * Configuration interface for hop 8 - */ 9 6 export interface Config { 10 - /** SSH server in format "user@host" */ 11 7 server: string; 12 - /** Remote path where files will be uploaded */ 13 8 remotePath: string; 14 - /** Base URL for accessing uploaded files */ 15 9 baseUrl: string; 16 10 } 17 11 ··· 26 20 27 21 const CONFIG_TEMPLATE = JSON.stringify(DEFAULT_CONFIG, null, 2); 28 22 29 - /** 30 - * Loads configuration from ~/.config/hop/config.json 31 - * Creates a default config file if it doesn't exist 32 - * @returns The loaded configuration 33 - * @throws Exits process if config is missing or invalid 34 - */ 35 23 export async function loadConfig(): Promise<Config> { 36 24 if (!existsSync(CONFIG_PATH)) { 37 25 await mkdir(CONFIG_DIR, { recursive: true });
-18
src/constants.ts
··· 1 - /** Application constants */ 2 - 3 - /** Length of hash to use for filenames (truncated from SHA256 hex) */ 4 1 export const HASH_LENGTH = 7; 5 - 6 - /** Video quality CRF (Constant Rate Factor) for ffmpeg VP9 encoding 7 - * Lower values = higher quality, larger file size 8 - * 30 is a good balance for screen recordings 9 - */ 10 2 export const VIDEO_QUALITY_CRF = 30; 11 - 12 - /** Maximum filename length (filesystem limit for most systems) */ 13 3 export const MAX_FILENAME_LENGTH = 255; 14 - 15 - /** Timeout for pbcopy command (clipboard operations) in milliseconds */ 16 4 export const PBCOPY_TIMEOUT_MS = 5000; 17 - 18 - /** Timeout for defaults command (reading macOS preferences) in milliseconds */ 19 5 export const DEFAULTS_TIMEOUT_MS = 5000; 20 - 21 - /** Timeout for screencapture screenshot command in milliseconds */ 22 6 export const SCREENSHOT_TIMEOUT_MS = 60000; 23 - 24 - /** Timeout for ffprobe command (getting video metadata) in milliseconds */ 25 7 export const FFPROBE_TIMEOUT_MS = 30000;
+14 -5
src/index.ts
··· 3 3 import { loadConfig } from "./config"; 4 4 import { generateHash, getFilename } from "./hash"; 5 5 import { uploadWithProgress } from "./upload"; 6 - import { copyToClipboard } from "./clipboard"; 6 + import { copyToClipboard, getClipboardContent, cleanupClipboardFile } from "./clipboard"; 7 7 import { captureScreenshot } from "./screenshot"; 8 8 import { captureRecording } from "./record"; 9 9 import { validateFilename } from "./utils"; ··· 136 136 } 137 137 138 138 if (!filePath) { 139 - console.error("Error: No file specified"); 140 - console.error("Run 'hop --help' for usage information"); 141 - process.exit(1); 139 + const clipboardContent = await getClipboardContent(); 140 + if (!clipboardContent) { 141 + console.error("Error: No file specified and clipboard is empty"); 142 + console.error("Run 'hop --help' for usage information"); 143 + process.exit(1); 144 + } 145 + filePath = clipboardContent.path; 142 146 } 143 147 144 148 if (keepFilename && customFilename) { ··· 183 187 } 184 188 185 189 const url = `${config.baseUrl}/${encodeURI(filename)}`; 186 - console.log(`✓ uploaded ${originalName} to ${url}`); 190 + console.log(`uploaded ${originalName} to ${url}`); 191 + 192 + // Clean up temp clipboard file if it was used 193 + if (filePath.startsWith('/tmp/hop-clipboard-')) { 194 + await cleanupClipboardFile(filePath); 195 + } 187 196 188 197 try { 189 198 await copyToClipboard(url);
+1 -12
src/record.ts
··· 7 7 import { VIDEO_QUALITY_CRF, FFPROBE_TIMEOUT_MS } from "./constants"; 8 8 import { hasRawMode } from "./types"; 9 9 10 - /** Options for screen recording */ 11 10 interface RecordingOptions { 12 - /** Whether to include microphone audio */ 13 11 audio: boolean; 14 12 } 15 13 16 - /** Recording state machine to prevent race conditions */ 17 14 type RecordingState = "idle" | "recording" | "finishing" | "cancelled"; 18 15 19 16 function generateRecordingFilename(extension: string): string { ··· 127 124 }); 128 125 } 129 126 130 - /** 131 - * Records the screen using macOS screencapture and converts to WebM 132 - * @param options Recording options 133 - * @returns Promise resolving to WebM file path on success, null on cancellation or error 134 - */ 135 127 export async function captureRecording( 136 128 options: RecordingOptions 137 129 ): Promise<string | null> { ··· 186 178 output: process.stdout 187 179 }); 188 180 189 - // Only enable raw mode if stdin supports it (not in compiled binary) 190 181 const stdinHasRawMode = hasRawMode(process.stdin); 191 182 if (stdinHasRawMode) { 192 183 process.stdin.setRawMode(true); ··· 194 185 } 195 186 196 187 const handleKeypress = (key: Buffer) => { 197 - // State machine prevents race conditions 198 188 if (state !== "recording") return; 199 189 200 190 if (key.toString() === "\r" || key.toString() === "\n") { ··· 209 199 process.stdin.on("data", handleKeypress); 210 200 211 201 const sigintHandler = () => { 212 - // State machine prevents double-handling 213 202 if (state !== "recording") return; 214 203 215 204 state = "cancelled"; ··· 284 273 } 285 274 process.stdin.pause(); 286 275 rl.close(); 287 - console.error(`\nError: screencapture failed - ${err.message}`); 276 + console.error(`screencapture failed: ${err.message}`); 288 277 resolve(null); 289 278 }); 290 279 });
-8
src/screenshot.ts
··· 2 2 import { join } from "path"; 3 3 import { getCaptureDirectory, formatTimestamp } from "./utils"; 4 4 5 - /** Options for screenshot capture */ 6 5 interface ScreenshotOptions { 7 - /** Whether to capture a region/window instead of fullscreen */ 8 6 region: boolean; 9 7 } 10 - 11 - /** 12 - * Captures a screenshot using macOS screencapture utility 13 - * @param options Screenshot capture options 14 - * @returns Promise resolving to file path on success, null on cancellation or error 15 - */ 16 8 17 9 function generateScreenshotFilename(): string { 18 10 const timestamp = formatTimestamp(new Date());
-9
src/upload.ts
··· 25 25 return RSYNC_ERRORS[code] || `rsync exited with code ${code}`; 26 26 } 27 27 28 - /** 29 - * Uploads a file to a remote server via rsync with progress display 30 - * @param localPath Path to the local file to upload 31 - * @param remoteDest Remote destination in format "server:path" 32 - * @param originalName Original filename for display purposes 33 - * @param verbose Whether to show verbose output 34 - * @returns Promise that resolves on success, rejects on failure 35 - * @throws Error if rsync fails 36 - */ 37 28 export async function uploadWithProgress( 38 29 localPath: string, 39 30 remoteDest: string,
-16
src/utils.ts
··· 3 3 import { homedir } from "os"; 4 4 import { DEFAULTS_TIMEOUT_MS } from "./constants"; 5 5 6 - /** Result type for operations that can fail */ 7 6 export type Result<T, E> = 8 7 | { ok: true; value: T } 9 8 | { ok: false; error: E }; 10 9 11 - /** 12 - * Formats a date into macOS-style timestamp string 13 - * Format: "YYYY-MM-DD at HH.MM.SS" 14 - */ 15 10 export function formatTimestamp(date: Date): string { 16 11 const year = date.getFullYear(); 17 12 const month = String(date.getMonth() + 1).padStart(2, "0"); ··· 23 18 return `${year}-${month}-${day} at ${hours}.${minutes}.${seconds}`; 24 19 } 25 20 26 - /** Validation errors for filenames */ 27 21 export type FilenameValidationError = 28 22 | "empty" 29 23 | "too_long" 30 24 | "path_traversal" 31 25 | "invalid_chars"; 32 26 33 - /** 34 - * Validates a custom filename to prevent path traversal and invalid characters 35 - * @param filename The filename to validate 36 - * @returns Result with validated filename or error type 37 - */ 38 27 export function validateFilename( 39 28 filename: string 40 29 ): Result<string, FilenameValidationError> { ··· 63 52 return { ok: true, value: filename.trim() }; 64 53 } 65 54 66 - /** 67 - * Gets the macOS screenshot capture directory from system preferences 68 - * Falls back to Desktop if not set or command fails 69 - * @returns Promise resolving to directory path 70 - */ 71 55 export async function getCaptureDirectory(): Promise<string> { 72 56 return new Promise((resolve) => { 73 57 const proc = spawn("defaults", ["read", "com.apple.screencapture", "location"]);