1 /*
2 * arcan-clipboard (aclip)
3 * Copyright 2017-2018, Björn Ståhl
4 * License: 3-Clause BSD, see COPYING file in arcan source repository.
5 * Description: Clipboard Integration tool
6 */
7 #include <arcan_shmif.h>
8 #include <getopt.h>
9 #include <signal.h>
10 #include "utf8.c"
11
12 static char* separator = "";
13
paste(struct arcan_shmif_cont * out,char * msg,bool cont)14 static void paste(struct arcan_shmif_cont* out, char* msg, bool cont)
15 {
16 arcan_event msgev = {
17 .ext.kind = ARCAN_EVENT(MESSAGE)
18 };
19
20 if (!out || !out->vidp || !msg)
21 return;
22
23 /* split into message size blocks, align backwards so full UTF8
24 * seqs are passed, else server may dismiss or kill us */
25 size_t len = strlen(msg);
26
27 uint32_t state = 0, codepoint = 0;
28 char* outs = msg;
29 size_t maxlen = sizeof(msgev.ext.message.data) - 1;
30
31 while (len > maxlen){
32 size_t i, lastok = 0;
33 state = 0;
34 for (i = 0; i <= maxlen - 1; i++){
35 if (UTF8_ACCEPT == utf8_decode(&state, &codepoint, (uint8_t)(msg[i])))
36 lastok = i;
37
38 if (i != lastok){
39 if (0 == i)
40 return;
41 }
42 }
43
44 memcpy(msgev.ext.message.data, outs, lastok);
45 msgev.ext.message.data[lastok] = '\0';
46 len -= lastok;
47 outs += lastok;
48 if (len)
49 msgev.ext.message.multipart = 1;
50 else
51 msgev.ext.message.multipart = cont;
52
53 arcan_shmif_enqueue(out, &msgev);
54 }
55
56 /* flush remaining */
57 if (len){
58 snprintf((char*)msgev.ext.message.data, maxlen, "%s", outs);
59 msgev.ext.message.multipart = cont;
60 arcan_shmif_enqueue(out, &msgev);
61 }
62 }
63
write_ev(arcan_event * ev,FILE * outf)64 static bool write_ev(arcan_event* ev, FILE* outf)
65 {
66 /* if it's a continuation, we can't write the separator yet, just find the
67 * end of the buffer or where the string was truncated and terminated */
68 if (ev->tgt.ioevs[0].iv != 0){
69 size_t i = 0;
70 for (; i < sizeof(ev->tgt.message); i++)
71 if (ev->tgt.message[i] == '\0')
72 break;
73 if (i == 0)
74 return false;
75 if (i == sizeof(ev->tgt.message))
76 i++;
77 fwrite(ev->tgt.message, i, 1, outf);
78 return false;
79 }
80 else{
81 fprintf(outf, "%s%s", ev->tgt.message, separator);
82 fflush(outf);
83 return true;
84 }
85 }
86
dispatch_event_outf(arcan_event * ev,FILE * outf)87 static int dispatch_event_outf(arcan_event* ev, FILE* outf)
88 {
89 switch (ev->tgt.kind){
90 case TARGET_COMMAND_MESSAGE:
91 if (write_ev(ev, outf))
92 return 1;
93 break;
94 case TARGET_COMMAND_NEWSEGMENT:
95 /* if we explicitly receive a new paste board due to some incoming stream */
96 break;
97 case TARGET_COMMAND_BCHUNK_IN:
98 break;
99 case TARGET_COMMAND_BCHUNK_OUT:
100 /* incoming binary blob, fd in ioevs[0].iv */
101 break;
102 case TARGET_COMMAND_STEPFRAME:
103 /* need to distinguish if we are an output segment or not, in the latter
104 * case, we just received a raw video buffer to work with */
105 break;
106 case TARGET_COMMAND_EXIT:
107 return -1;
108 break;
109 default:
110 break;
111 }
112 return 0;
113 }
114
dispatch_event_outcmd(struct arcan_event * ev,const char * cmd)115 static int dispatch_event_outcmd(struct arcan_event* ev, const char* cmd)
116 {
117 static FILE* outf;
118
119 switch(ev->tgt.kind){
120 case TARGET_COMMAND_MESSAGE:{
121 if (!outf && !(outf = popen(cmd, "w")))
122 return -1;
123 write_ev(ev, outf);
124 if (ev->tgt.ioevs[0].iv == 0){
125 fclose(outf);
126 outf = NULL;
127 return 1;
128 }
129 }
130 break;
131 case TARGET_COMMAND_EXIT:
132 return -1;
133 break;
134 default:
135 break;
136 }
137 return 0;
138 }
139
140 static const struct option longopts[] = {
141 { "help", no_argument, NULL, 'h'},
142 { "in", no_argument, NULL, 'i'},
143 { "in-data", required_argument, NULL, 'I'},
144 { "out", no_argument, NULL, 'o'},
145 { "exec", required_argument, NULL, 'e'},
146 { "separator", required_argument, NULL, 'p'},
147 { "loop", required_argument, NULL, 'l'},
148 { "display", required_argument, NULL, 'd'},
149 { "silent", no_argument, NULL, 's'},
150 { NULL, no_argument, NULL, 0 }
151 };
152
usage()153 static void usage()
154 {
155 printf("Usage: aclip [-hioe:s:l:d:]\n"
156 "-h \t--help \tthis text\n"
157 "-i \t--in \tread/validate UTF-8 from standard input and copy to clipboard\n"
158 "-I arg\t--in-data arg \tread/validate UTF-8 and copy to clipboard\n"
159 "-o \t--out \tflush received pastes to stdout\n"
160 "-e arg\t--exec arg \tpopen [arg] on received pastes and flush to its stdin\n"
161 "-p arg\t--separator arg\ttreat [arg] as paste- separator separator (-l >= 0)\n"
162 "-l arg\t--loop arg \texit after [arg] discrete paste operations (-0, never exit)\n"
163 "-s \t--silent \tclose stdout and fork into background\n"
164 "-d arg\t--display arg \tuse [arg] as connection path istead of ARCAN_CONNPATH env\n"
165 );
166 }
167
main(int argc,char ** argv)168 int main(int argc, char** argv)
169 {
170 int loop_counter = -1;
171
172 if (argc == 1){
173 usage();
174 return EXIT_FAILURE;
175 }
176
177 bool use_stdin = false, use_stdout = false, silence = false;
178 char* exec_cmd = NULL, (* copy_arg) = NULL;
179
180 int ch;
181 while((ch = getopt_long(argc, argv, "hisI:oe:p:l:d:", longopts, NULL)) >= 0)
182 switch(ch){
183 case 'h' : usage(); return EXIT_SUCCESS;
184 case 'i' : use_stdin = true; break;
185 case 'o' : use_stdout = true; break;
186 case 's' : silence = true; break;
187 case 'I' : {
188 if (copy_arg)
189 free(copy_arg);
190 copy_arg = strdup(optarg);
191 }
192 break;
193 case 'e' : {
194 if (exec_cmd)
195 free(exec_cmd);
196 exec_cmd = strdup(optarg);
197 }
198 break;
199 case 'd' : setenv("ARCAN_CONNPATH", optarg, 1); break;
200 case 'l' : loop_counter = strtoul(optarg, NULL, 10); break;
201 case 'p' : separator = strdup(optarg ? optarg : ""); break;
202 default:
203 break;
204 }
205
206 if (!use_stdin && !use_stdout && !copy_arg){
207 fprintf(stderr, "neither [-i], [-I arg] nor [-o] specified.\n");
208 usage();
209 return EXIT_FAILURE;
210 }
211
212 /* Something of an inconvenience right now, in order to get access to a proper
213 * pasteboard (full A/V support) we need a non-output primary segment through
214 * which that request can be pushed. If we get one, we can just switch the
215 * struct that processes paste- output though. */
216 struct arcan_shmif_cont con = arcan_shmif_open_ext(
217 SHMIF_ACQUIRE_FATALFAIL, NULL, (struct shmif_open_ext){
218 .type = SEGID_CLIPBOARD,
219 .title = "",
220 .ident = ""
221 }, sizeof(struct shmif_open_ext));
222 arcan_shmif_signal(&con, SHMIF_SIGVID);
223
224 if (signal(SIGCHLD, SIG_IGN) == SIG_ERR){
225 fprintf(stderr, "ign on sigchld failed\n");
226 return EXIT_FAILURE;
227 }
228
229 if (silence){
230 fclose(stdout);
231 if (fork() != 0)
232 return EXIT_SUCCESS;
233 }
234
235 /*
236 * Two little "gotcha's" here, one is that a lot of the event handler / resource
237 * allocation schemes are actually deferred until the first transfer signal.
238 * The other is a possible race: _drop pulls the 'dms' and the client is
239 * considered dead. Whatever is pending on the event queue at the moment will
240 * never be processed and our paste- message gets lost, so we want synch-
241 * delivery here. See the approach in drop_exit at the end.
242 */
243 if (copy_arg){
244 paste(&con, copy_arg, false);
245 }
246
247 /* "worst" case, both input and output - fork, block-read from stdin and put to
248 * output queue. cont can be shared in this corner case as the parent will still
249 * be alive due to the in/out event-queue separation. */
250 bool drop_exit = true;
251 if (use_stdin && use_stdout){
252 pid_t pv = fork();
253 if (pv > 0){
254 use_stdin = false;
255 }
256 else if (pv == 0){
257 use_stdout = false;
258 drop_exit = false;
259 }
260 }
261
262 /* block/flush stdin and split into paste- messages */
263 arcan_event ev;
264 if (use_stdin){
265 char buf[sizeof(ev.ext.message.data) * 4];
266 while(!feof(stdin) && !ferror(stdin)){
267 size_t ntr = fread(buf, 1, sizeof(buf)-1, stdin);
268 buf[ntr] = '\0';
269 if (ntr && ntr < sizeof(buf)-1)
270 paste(&con, buf, !(feof(stdin) || ferror(stdin)));
271 }
272 }
273
274 /* normal event loop and trigger when complete messages are received */
275 if (use_stdout){
276 while(arcan_shmif_wait(&con, &ev) > 0){
277 if (ev.category == EVENT_TARGET){
278 int sv = exec_cmd ?
279 dispatch_event_outcmd(&ev, exec_cmd) :
280 dispatch_event_outf(&ev, stdout);
281 if (-1 == sv || (1 == sv && loop_counter == 1))
282 break;
283 if (loop_counter > 0)
284 loop_counter--;
285 }
286 }
287 }
288
289 /* defer the drop call until the eventqueue is empty, not a very pretty
290 * solution but this is a rather fringe use-case (connect, queue one
291 * event, shutdown) to play out in an asynch setting */
292 if (drop_exit){
293 while (con.addr && con.addr->dms &&
294 con.addr->parentevq.front != con.addr->parentevq.back)
295 arcan_shmif_signal(&con, SHMIF_SIGVID);
296 arcan_shmif_drop(&con);
297 }
298
299 return EXIT_SUCCESS;
300 }
301