1 // FIXME: catch the suspend signal (ctrl+z) and forward it inside
2 // FIXME: i should be able to change the title from the outside too
3 /++
4 	This is a GNU Screen style program to multiplex and provide remote attach/detach
5 	support to a terminal emulator backend.
6 
7 	It works with two pieces: sessions and screens. A session is a collection of screens
8 	and a screen is one backend terminal emulator.
9 
10 	attach must be run inside a terminal emulator itself. For best results, use the GUI
11 	frontend provided in this package, but it will also work on most others like the linux
12 	console or xterm. You can also use a nested terminal emulator with it if you want.
13 
14 	Controls by default are based on screen. Differences are:
15 
16 	C-a D: detach the current screen from the session
17 	C-a C: attach a specific screen to the session
18 
19 	C-a t: toggle taskbar. The taskbar will show a tab in green if it has a beep.
20 
21 	C-a <colon>: start command line
22 		attach socket_name
23 
24 	You can edit session files in ~/.detachable-terminals with a text editor.
25 +/
26 module arsd.terminalemulatorattachutility;
27 
28 import arsd.detachableterminalemulatormessage;
29 
30 import arsd.terminal;
31 
32 static import std.file;
33 static import core.stdc.stdlib;
34 import std.conv;
35 import std.socket;
36 
37 /*
38 	FIXME: error messages aren't displayed when you attach and the socket cannot be opened
39 
40 	FIXME: activity/silence watching should make sense out of the box so default on doesn't annoy me
41 	FIXME: when a session is remote attached, it steals all the screens. this causes the existing one
42 	       to write out a blank session file, meaning it won't be stolen back easily. This should change
43 	       so it is aware of the remote detach and just leaves things, or gracefully exits upon request.
44 
45 	fun facts:
46 		SIGTSTP is the one we catch
47 		SIGCONT is the one to send to make it continue
48 	Session file example:
49 
50 	title: foo
51 	icon: bar.png
52 	cwd: /home/me
53 	screens: foo bar baz
54 
55 	FIXME: support icon changing and title changing and DEMANDS_ATTENTION support
56 	FIXME: if a screen detaches because it is being attached somewhere else, we should note that somehow
57 
58 	FIXME: support the " key
59 	FIXME: watch for change in silence automatically
60 	FIXME: allow suppression of beeps
61 
62 
63 	so what do i want for activity noticing...
64 		if it has been receiving user input, give it a bigger time out
65 
66 		if there's been no output for a minute, consider it silent
67 		if there has been output in the last minute, it is not silent
68 
69 		if it is less than a minute old, it is neither silent nor loud yet
70 */
71 
72 struct Session {
73 	import core.sys.posix.sys.types;
74 	pid_t pid; // a hacky way to keep track of who has this open right now for remote detaching
75 
76 	// preserved in the file
77 	string title;
78 	string icon;
79 	string cwd;
80 	string[] screens;
81 	string[] screensTitlePrefixes;
82 	string autoCommand;
83 	dchar escapeCharacter = 1;
84 	bool showingTaskbar = true;
85 
86 	/// if set to true, screens are counted from zero when jumping with the number keys (like GNU screen)
87 	/// otherwise, screens count from one. I like one because it is more convenient with the left hand
88 	/// and the numbers match up visually with the tabs, but the zero based gnu screen habit is hard to break.
89 	bool zeroBasedCounting;
90 
91 	// the filename
92 	string sname;
93 
94 	bool mouseTrackingOn;
95 
96 	// not preserved in the file
97 	ChildTerminal[] children;
98 	int activeScreen;
99 
100 	void saveToFile() {
101 		import std.stdio;
102 		import std..string;
103 		if(sname.length == 0) return;
104 		auto file = File(socketDirectoryName() ~ "/" ~ sname ~ ".session", "wt");
105 		file.writeln("title: ", title);
106 		file.writeln("icon: ", icon);
107 		file.writeln("cwd: ", cwd);
108 		file.writeln("autoCommand: ", autoCommand);
109 		file.writeln("escapeCharacter: ", cast(dchar) (escapeCharacter + 'a' - 1));
110 		file.writeln("showingTaskbar: ", showingTaskbar);
111 		file.writeln("zeroBasedCounting: ", zeroBasedCounting);
112 		file.writeln("pid: ", pid);
113 		file.writeln("activeScreen: ", activeScreen);
114 		file.writeln("screens: ", join(screens, " "));
115 		file.writeln("screensTitlePrefixes: ", join(screensTitlePrefixes, "; "));
116 	}
117 
118 	void readFromFile() {
119 		import std.stdio;
120 		import std..string;
121 		import std.file;
122 		if(sname.length == 0) return;
123 		if(!std.file.exists(socketDirectoryName() ~ "/" ~ sname ~ ".session"))
124 			return;
125 		auto file = File(socketDirectoryName() ~ "/" ~ sname ~ ".session", "rt");
126 		foreach(line; file.byLine) {
127 			auto idx = indexOf(line, ":");
128 			if(idx == -1)
129 				continue;
130 			auto lhs = strip(line[0 .. idx]);
131 			auto rhs = strip(line[idx + 1 .. $]);
132 			switch(lhs) {
133 				case "title": title = rhs.idup; break;
134 				case "cwd": cwd = rhs.idup; break;
135 				case "autoCommand": autoCommand = rhs.idup; break;
136 				case "icon": icon = rhs.idup; break;
137 				case "escapeCharacter":
138 					import std.utf;
139 					escapeCharacter = decodeFront(rhs) + 1 - 'a';
140 				break;
141 				case "showingTaskbar": showingTaskbar = rhs == "true"; break;
142 				case "zeroBasedCounting": zeroBasedCounting = rhs == "true"; break;
143 				case "pid": pid = to!int(rhs); break;
144 				case "activeScreen": activeScreen = to!int(rhs); break;
145 				case "screens": screens = split(rhs.idup, " "); break;
146 				case "screensTitlePrefixes": screensTitlePrefixes = split(line[idx + 1 .. $].stripLeft.idup, "; "); break;
147 				default: continue;
148 			}
149 		}
150 	}
151 
152 	void saveUpdatedSessionToFile() {
153 		if(this.sname !is null) {
154 			this.screens = null;
155 			this.screensTitlePrefixes = null;
156 			foreach(child; this.children) {
157 				if(child.socket !is null) {
158 					this.screens ~= child.socketName;
159 				} else {
160 					this.screens ~= "[vacant]";
161 				}
162 
163 				this.screensTitlePrefixes ~= child.titlePrefix;
164 			}
165 			this.saveToFile();
166 		}
167 	}
168 }
169 
170 import core.stdc.time;
171 
172 struct ChildTerminal {
173 	Socket socket;
174 	string title;
175 
176 	string socketName;
177 	// tab image
178 
179 	string titlePrefix;
180 
181 	bool demandsAttention;
182 
183 	// for mouse click detection
184 	int x;
185 	int x2;
186 
187 	// for detecting changes in output
188 	time_t lastActivity;
189 	bool lastWasSilent;
190 }
191 
192 extern(C) nothrow static @nogc
193 void detachable_child_dead(int) {
194 	import core.sys.posix.sys.wait;
195 	wait(null);
196 }
197 
198 bool stopRequested;
199 
200 extern(C) nothrow static @nogc
201 void stop_requested(int) {
202 	stopRequested = true;
203 }
204 
205 
206 bool debugMode;
207 bool outputPaused;
208 int previousScreen = 0;
209 bool running = true;
210 Socket socket;
211 
212 void main(string[] args) {
213 
214 	Session session;
215 
216 	if(args.length > 1 && args[1] != "--list" && args[1] != "--cleanup") {
217 		import std.algorithm : endsWith;
218 
219 		if(args.length == 2 && !endsWith(args[1], ".socket")) {
220 			// load the given argument as a session
221 			session.sname = args[1];
222 		} else {
223 			// make an anonymous session with the listed screens as sockets
224 			foreach(arg; args[1 .. $])
225 				session.screens ~= arg;
226 		}
227 	} else {
228 		// list the available sockets and sessions...
229 		import std.file, std.process, std.stdio;
230 		bool found = false;
231 		auto dirName = socketDirectoryName();
232 
233 		string[] sessions;
234 		string[] sockets;
235 
236 		foreach(string name; dirEntries(dirName, SpanMode.shallow)) {
237 			name = name[dirName.length + 1 .. $];
238 			if(name[$-1] == 'n')
239 				sessions ~= name[0 .. $ - ".session".length];
240 			else
241 				sockets ~= name[0 .. $ - ".socket".length];
242 			found = true;
243 		}
244 		if(found) {
245 			string[string] associations;
246 			foreach(sessionName; sessions) {
247 				auto sess = Session();
248 				sess.sname = sessionName;
249 				sess.readFromFile();
250 
251 				foreach(s; sess.screens)
252 					associations[s] = sessionName;
253 
254 				if(args.length == 2 && args[1] == "--cleanup") {
255 
256 				} else
257 					writefln("%20s\t%d\t%d", sessionName, sess.pid, sess.screens.length);
258 			}
259 
260 			foreach(socketName; sockets) {
261 				if(args.length == 2 && args[1] == "--cleanup") {
262 					if(socketName !in associations) {
263 						import core.stdc.stdlib;
264 						static import std.file;
265 						if(std.file.exists("/proc/" ~ socketName))
266 							system(("attach " ~ socketName ~ ".socket" ~ "\0").ptr);
267 						else
268 							std.file.remove(socketDirectoryName() ~ "/" ~ socketName ~ ".socket");
269 					}
270 				} else {
271 					writefln("%s.socket\t\t%s", socketName, (socketName in associations) ? associations[socketName] : "[detached]");
272 
273 					static import std.file;
274 					if(std.file.exists("/proc/" ~ socketName)) {
275 						auto newSocket = connectTo(socketName, false);
276 						if(newSocket) {
277 							sendSimpleMessage(newSocket, InputMessage.Type.RequestStatus);
278 							char[1024] buffer;
279 							auto read = newSocket.receive(buffer[]);
280 							while(read > 0) {
281 								writef("%s", buffer[0 .. read]);
282 								read = newSocket.receive(buffer[]);
283 							}
284 							newSocket.close();
285 						}
286 					}
287 				}
288 			}
289 		} else {
290 			writeln("No screens found");
291 		}
292 		return;
293 	}
294 
295 	import core.sys.posix.signal;
296 	signal(SIGPIPE, SIG_IGN);
297 	signal(SIGCHLD, &detachable_child_dead);
298 	signal(SIGTSTP, &stop_requested);
299 
300 	import std.process;
301 
302 	// FIXME: set up a FIFO or something so we can receive commands
303 	// pertaining to the whole session like detach... or something.
304 	// if we do that it will need a way to find it by session name
305 	// or by pid. Maybe a symlink.
306 
307 	session.cwd = std.file.getcwd();
308 	session.title = session.sname;
309 	session.readFromFile();
310 
311 	if(session.pid) {
312 		// detach the old session
313 		kill(session.pid, SIGHUP);
314 		import core.sys.posix.unistd;
315 		usleep(1_000_000); // give the old process a chance to die
316 		session.readFromFile();
317 	}
318 
319 	session.pid = thisProcessID();
320 
321 	if(session.cwd.length)
322 		std.file.chdir(session.cwd);
323 
324 	bool good = false;
325 
326 	foreach(idx, sname; session.screens) {
327 		if(sname == "[vacant]") {
328 			session.children ~= ChildTerminal(null, sname, sname, idx < session.screensTitlePrefixes.length ? session.screensTitlePrefixes[idx] : null);
329 			continue;
330 		}
331 		auto socket = connectTo(sname);
332 		if(socket is null)
333 			sname = "[failed]";
334 		else {
335 			good = true;
336 			sendSimpleMessage(socket, InputMessage.Type.Attach);
337 		}
338 		session.children ~= ChildTerminal(socket, sname, sname, idx < session.screensTitlePrefixes.length ? session.screensTitlePrefixes[idx] : null);
339 
340 		// we should scan inactive sockets for:
341 		// 1) a bell
342 		// 2) a title change
343 		// 3) a window icon change
344 
345 		// as these can all be reflected in the tab listing
346 	}
347 
348 	if(session.children.length == 0)
349 		session.children ~= ChildTerminal(null, null, null, null);
350 
351 	assert(session.children.length);
352 
353 	if(!good) {
354 		if(session.children[0].socketName == "[vacant]" || session.children[0].socketName == "[failed]")
355 			session.children[0].socketName = null;
356 		session.children[0].socket = connectTo(session.children[0].socketName);
357 		if(auto socket = session.children[0].socket) {
358 			sendSimpleMessage(socket, InputMessage.Type.Attach);
359 
360 			if(session.autoCommand.length)
361 				sendSimulatedInput(socket, session.autoCommand);
362 		}
363 	}
364 
365 
366 	session.saveUpdatedSessionToFile(); // saves the new PID
367 
368 	// doing these just to get it in the state i want
369 	auto terminal = Terminal(ConsoleOutputType.cellular);
370 	auto input = RealTimeConsoleInput(&terminal, ConsoleInputFlags.raw | ConsoleInputFlags.allInputEvents);
371 	try {
372 
373 	// We got a bit beyond what Terminal does and also disable kernel flow
374 	// control, allowing us to capture ^S and ^Q too.
375 	// There's no need to reset at end of scope btw because RealTimeConsoleInput's dtor
376 	// resets to original state, saved before we make this change anyway.
377 	{
378 		import core.sys.posix.termios;
379 		// padding because I'm not sure druntime's termios is the correct size
380 		ubyte[128] padding;
381 		termios old;
382 		ubyte[128] padding2;
383 		tcgetattr(0 /* terminal.fdIn */, &old);
384 		old.c_iflag &= ~(IXON | IXOFF | ISIG);
385 		old.c_cc[VQUIT] = 0; // disable the ctrl+\ signal so it can be handled on the inner layer too
386 		tcsetattr(0, TCSANOW, &old);
387 	}
388 
389 	// then all we do is forward data to/from stdin to the pipe
390 	import core.stdc.errno;
391 	import core.sys.posix.unistd;
392 	import core.sys.posix.sys.select;
393 
394 	if(session.activeScreen < session.children.length && session.children[session.activeScreen].socket !is null) {
395 		setActiveScreen(&terminal, session, cast(int) session.activeScreen, true);
396 	} else {
397 		foreach(idx, child; session.children)
398 			if(child.socket !is null) {
399 				setActiveScreen(&terminal, session, cast(int) idx, true);
400 				break;
401 			}
402 	}
403 	if(socket is null)
404 		return;
405 
406 	// sets the icon, if set
407 	if(session.icon.length) {
408 		import arsd.terminalextensions;
409 		changeWindowIcon(&terminal, session.icon);
410 	}
411 
412 	while(running) {
413 		if(stopRequested) {
414 			stopRequested = false;
415 
416 			InputMessage im;
417 			im.type = InputMessage.Type.CharacterPressed;
418 			im.characterEvent.character = 26; // ctrl+z
419 			im.eventLength = im.sizeof;
420 			write(socket.handle, &im, im.eventLength);
421 		}
422 
423 		terminal.flush();
424 		ubyte[4096] buffer;
425 		fd_set rdfs;
426 		FD_ZERO(&rdfs);
427 
428 		FD_SET(0, &rdfs);
429 		int maxFd = 0;
430 		foreach(child; session.children) {
431 			if(child.socket !is null) {
432 				FD_SET(child.socket.handle, &rdfs);
433 				if(child.socket.handle > maxFd)
434 					maxFd = child.socket.handle;
435 			}
436 		}
437 
438 		timeval timeout;
439 		timeout.tv_sec = 10;
440 
441 		auto ret = select(maxFd + 1, &rdfs, null, null, &timeout);
442 		if(ret == -1) {
443 			if(errno == 4) { // EAGAIN
444 				while(running && (interrupted || windowSizeChanged || hangedUp))
445 					handleEvent(&terminal, session, input.nextEvent(), socket);
446 				continue; // EINTR
447 			}
448 			else throw new Exception("select");
449 		}
450 
451 		bool redrawTaskbar = false;
452 
453 		if(ret) {
454 
455 			if(FD_ISSET(0, &rdfs)) {
456 				// the terminal is ready, we'll call next event here
457 				while(running && input.anyInput_internal())
458 				handleEvent(&terminal, session, input.nextEvent(), socket);
459 			}
460 
461 			foreach(ref child; session.children) if(child.socket !is null) {
462 				if(FD_ISSET(child.socket.handle, &rdfs)) {
463 					// data from the pty should be forwarded straight out
464 					auto len = read(child.socket.handle, buffer.ptr, cast(int) 2);
465 					if(len <= 0) {
466 						// probably end of file or something cuz the child exited
467 						// we should switch to the next possible screen
468 						//throw new Exception("closing cuz of bad read " ~ to!string(errno) ~ " " ~ to!string(len));
469 						closeSocket(&terminal, session, child.socket);
470 
471 						continue;
472 					}
473 
474 					assert(len == 2); // should be a frame
475 					// unpack the frame
476 					OutputMessageType messageType = cast(OutputMessageType) buffer[0];
477 					ubyte messageLength = buffer[1];
478 					if(messageLength) {
479 						// unpack the message
480 						int where = 0;
481 						while(where < messageLength) {
482 							len = read(child.socket.handle, buffer.ptr + where, messageLength - where);
483 							if(len <= 0) assert(0);
484 							where += len;
485 						}
486 						assert(where == messageLength);
487 					}
488 
489 
490 					void handleDataFromTerminal() {
491 						/* read just for stuff in the background like bell or title change */
492 						int lastEsc = -1;
493 						int cut1 = 0, cut2 = 0;
494 						foreach(bidx, b; buffer[0 .. messageLength]) {
495 							if(b == '\033')
496 								lastEsc = cast(int) bidx;
497 
498 							if(b == '\007') {
499 								if(lastEsc != -1) {
500 									auto pieces = cast(char[]) buffer[lastEsc .. bidx];
501 									cut1 = lastEsc;
502 									cut2 = 0;
503 									lastEsc = -1;
504 
505 									// anything longer is just unreasonable
506 									if(pieces.length > 4 && pieces.length < 120)
507 									if(pieces[1] == ']' && pieces[2] == '0' && pieces[3] == ';') {
508 										child.title = pieces[4 .. $].idup;
509 										redrawTaskbar = true;
510 
511 										cut2 = cast(int) bidx;
512 									}
513 								}
514 								if(child.socket !is socket) {
515 									child.demandsAttention = true;
516 									redrawTaskbar = true;
517 								}
518 							}
519 						}
520 
521 						// activity on the active screen needs to be forwarded
522 						// to the actual terminal so the user can see it too
523 						if(!outputPaused && child.socket is socket) {
524 							void writeOut(ubyte[] toWrite) {
525 								int len = cast(int) toWrite.length;
526 								while(len > 0) {
527 									if(!debugMode) {
528 										auto wrote = write(1, toWrite.ptr, len);
529 										if(wrote <= 0)
530 											throw new Exception("write");
531 										toWrite = toWrite[wrote .. $];
532 										len -= wrote;
533 									} else {import std.stdio; writeln(to!string(buffer[0..len])); len = 0;}
534 								}
535 							}
536 
537 							// FIXME
538 							if(false && cut2 > cut1) {
539 								// cut1 .. cut2 should be sliced out of the final output
540 								// a title change isn't necessarily desirable directly since
541 								// we do it in the session
542 								writeOut(buffer[0 .. cut1]);
543 								writeOut(buffer[cut2 + 1 .. messageLength]);
544 							} else {
545 								writeOut(buffer[0 .. messageLength]);
546 							}
547 						}
548 
549 						/+
550 						/* there's still new activity here */
551 						if(child.lastWasSilent && child.lastActivity) {
552 							child.demandsAttention = true;
553 							redrawTaskbar = true;
554 							child.lastWasSilent = false;
555 						}
556 						child.lastActivity = time(null);
557 						+/
558 					}
559 
560 					final switch(messageType) {
561 						case OutputMessageType.NULL:
562 							// should never happen
563 							assert(0);
564 						//break;
565 						case OutputMessageType.dataFromTerminal:
566 							handleDataFromTerminal();
567 						break;
568 						case OutputMessageType.remoteDetached:
569 							// FIXME: this should be done on a session level
570 
571 							// but the idea is if one is remote detached, they all are,
572 							// so we should just terminate immediately as to not write a new file
573 							return;
574 						//break;
575 						case OutputMessageType.mouseTrackingOn:
576 							session.mouseTrackingOn = true;
577 						break;
578 						case OutputMessageType.mouseTrackingOff:
579 							session.mouseTrackingOn = false;
580 						break;
581 					}
582 				} else {
583 					/+
584 					/* there was not any new activity, see if it has become silent */
585 					if(child.lastActivity && !child.lastWasSilent && time(null) - child.lastActivity > 10) {
586 						child.demandsAttention = true;
587 						child.lastWasSilent = true;
588 						redrawTaskbar = true;
589 					}
590 					+/
591 				}
592 			}
593 		} else {
594 			/+
595 			// it timed out, everybody is silent now
596 			foreach(ref child; session.children) {
597 				if(!child.lastWasSilent && child.lastActivity) {
598 					child.demandsAttention = true;
599 					child.lastWasSilent = true;
600 					redrawTaskbar = true;
601 				}
602 			}
603 			+/
604 		}
605 
606 		if(redrawTaskbar)
607 			drawTaskbar(&terminal, session);
608 	}
609 
610 	session.pid = 0; // we're terminating, don't keep the pid anymore
611 	session.saveUpdatedSessionToFile();
612 	 } catch(Throwable t) {
613 		terminal.writeln("\n\n\n", t);
614 		input.getch();
615 		input.getch();
616 		input.getch();
617 		input.getch();
618 		input.getch();
619 	 }
620 }
621 
622 
623 Socket connectTo(ref string sname, in bool spawn = true) {
624 	Socket socket;
625 
626 	if(sname.length) {
627 		try {
628 			socket = new Socket(AddressFamily.UNIX, SocketType.STREAM);
629 			socket.connect(new UnixAddress(socketFileName(sname)));
630 		} catch(Exception e) {
631 			socket = null;
632 		}
633 	}
634 
635 	if(socket is null && spawn) {
636 		// if it can't connect, we'll spawn a new backend to control it
637 		import core.sys.posix.unistd;
638 		if(auto pid = fork()) {
639 			import std.conv;
640 			if(sname.length == 0)
641 				sname = to!string(pid);
642 			int tries = 3;
643 			while(tries > 0 && socket is null) {
644 				// give the child a chance to get started...
645 				import core.thread;
646 				Thread.sleep(dur!"msecs"(25));
647 
648 				// and try to connect
649 				socket = connectTo(sname, false);
650 				tries--;
651 			}
652 		} else {
653 			// child
654 
655 			import core.sys.posix.fcntl;
656 			auto n = open("/dev/null", O_RDONLY);
657 			auto n2 = open("/dev/null", O_WRONLY);
658 			assert(n >= 0);
659 			import core.stdc.errno;
660 			assert(n2 >= 0, to!string(errno));
661 			dup2(n, 0);
662 			dup2(n2, 1);
663 
664 			// also detach from the calling foreground process group
665 			// because otherwise SIGINT will be sent to this too and kill
666 			// it instead of just going to the parent and being translated
667 			// into a ctrl+c input for the child.
668 
669 			setpgid(0, 0);
670 
671 			if(true) {
672 				// and run the detachable backend now
673 
674 				{
675 				// If we don't do this, events will get mixed up because
676 				// the pipes handles would be inherited across fork.
677 				import arsd.eventloop;
678 				openNewEventPipes();
679 				}
680 
681 
682 				// also changing the command line as seen in the shell ps command
683 				import core.runtime;
684 				auto cArgs = Runtime.cArgs;
685 				if(cArgs.argc) {
686 					import core.stdc..string;
687 					auto toOverwritePtr = cArgs.argv[0];
688 					auto toOverwrite = toOverwritePtr[0 .. strlen(toOverwritePtr) + 1];
689 					toOverwrite[] = 0;
690 					auto newName = "ATTACH";
691 					if(newName.length > toOverwrite.length - 1)
692 						newName = newName[0 .. toOverwrite.length - 1]; // leave room for a 0 terminator
693 					toOverwrite[0 .. newName.length] = newName[];
694 				}
695 
696 				// and calling main
697 				import arsd.detachableterminalemulator;
698 				try {
699 					detachableMain(["ATTACH", sname]);
700 				} catch(Throwable t) {
701 
702 				}
703 
704 				core.stdc.stdlib.exit(0); // we want this process dead like it would be with exec()
705 			}
706 
707 			// alternatively, but this requires a separate binary:
708 			// compile it separately with -version=standalone_detachable if you want it (is better for debugging btw) then use this code
709 			/*
710 			auto proggie = "/home/me/program/terminal-emulator/detachable";
711 			if(execl(proggie.ptr, proggie.ptr, (sname ~ "\0").ptr, 0))
712 				throw new Exception("wtf " ~ to!string(errno));
713 			*/
714 		}
715 	}
716 	return socket;
717 }
718 
719 
720 void drawTaskbar(Terminal* terminal, ref Session session) {
721 	static string lastTitleDrawn;
722 	bool anyDemandAttention = false;
723 	if(session.showingTaskbar) {
724 		terminal.writeStringRaw("\0337"); // save cursor
725 		scope(exit) {
726 			terminal.writeStringRaw("\0338"); // restore cursor
727 		}
728 
729 		int spaceRemaining = terminal.width;
730 		terminal.moveTo(0, terminal.height - 1);
731 		//terminal.writeStringRaw("\033[K"); // clear line
732 		terminal.color(Color.blue, Color.white, ForceOption.alwaysSend, true);
733 		terminal.write("  "); //"+ ");
734 		spaceRemaining-=2;
735 		spaceRemaining--; // save space for the close button on the end
736 
737 		foreach(idx, ref child; session.children) {
738 			child.x = terminal.width - spaceRemaining - 1;
739 			terminal.color(Color.blue, child.demandsAttention ? Color.green : Color.white, ForceOption.automatic, idx != session.activeScreen);
740 			terminal.write(" ");
741 			spaceRemaining--;
742 
743 			anyDemandAttention = anyDemandAttention || child.demandsAttention;
744 
745 			int size = 10;
746 			if(spaceRemaining < size)
747 				size = spaceRemaining;
748 			if(size <= 2)
749 				break;
750 
751 			if(child.title.length == 0)
752 				child.title = "Screen " ~ to!string(idx + 1);
753 
754 			auto dispTitle = child.titlePrefix.length ? (child.titlePrefix ~ child.title) : child.title;
755 
756 			if(dispTitle.length <= size-2) {
757 				terminal.write(dispTitle);
758 				foreach(i; dispTitle.length .. size-2)
759 					terminal.write(" ");
760 			} else {
761 				terminal.write(dispTitle[0 .. size-2]);
762 			}
763 			terminal.write("  ");
764 			spaceRemaining -= size;
765 			child.x2 = terminal.width - spaceRemaining - 1;
766 			if(spaceRemaining == 0)
767 				break;
768 		}
769 		terminal.color(Color.blue, Color.white, ForceOption.automatic, true);
770 
771 		foreach(i; 0 .. spaceRemaining)
772 			terminal.write(" ");
773 		terminal.write(" ");//"X");
774 	}
775 
776 	if(anyDemandAttention) {
777 		 import std.process;
778 		 if(environment["TERM"] != "linux")
779 			terminal.writeStringRaw("\033]5001;1\007");
780 	}
781 
782 	string titleToDraw;
783 	if(session.title.length)
784 		titleToDraw = session.title ~ " - " ~ session.children[session.activeScreen].title;
785 	else
786 		titleToDraw = session.children[session.activeScreen].title;
787 
788 	// FIXME
789 	if(true || lastTitleDrawn != titleToDraw) {
790 		lastTitleDrawn = titleToDraw;
791 		terminal.setTitle(titleToDraw);
792 	}
793 }
794 
795 int nextScreen(ref Session session) {
796 	foreach(i; session.activeScreen + 1 .. session.children.length)
797 		if(session.children[i].socket !is null)
798 			return cast(int) i;
799 	foreach(i; 0 .. session.activeScreen)
800 		if(session.children[i].socket !is null)
801 			return cast(int) i;
802 	return session.activeScreen;
803 }
804 
805 int nextScreenBackwards(ref Session session) {
806 	foreach_reverse(i; 0 .. session.activeScreen)
807 		if(session.children[i].socket !is null)
808 			return cast(int) i;
809 	foreach_reverse(i; session.activeScreen + 1 .. session.children.length)
810 		if(session.children[i].socket !is null)
811 			return cast(int) i;
812 	return session.activeScreen;
813 }
814 
815 void attach(Terminal* terminal, ref Session session, string sname) {
816 	int position = -1;
817 	foreach(idx, child; session.children)
818 		if(child.socket is null) {
819 			position = cast(int) idx;
820 			break;
821 		}
822 	if(position == -1) {
823 		position = cast(int) session.children.length;
824 		session.children ~= ChildTerminal();
825 	}
826 
827 	// spin off the child process
828 
829 	auto newSocket = connectTo(sname, true);
830 	if(newSocket) {
831 		sendSimpleMessage(newSocket, InputMessage.Type.Attach);
832 
833 		session.children[position] = ChildTerminal(newSocket, sname, sname);
834 		setActiveScreen(terminal, session, position);
835 
836 		if(session.autoCommand.length)
837 			sendSimulatedInput(newSocket, session.autoCommand);
838 	}
839 }
840 
841 void sendSimpleMessage(Socket socket, InputMessage.Type type) {
842 	InputMessage im;
843 	im.eventLength = InputMessage.type.offsetof + InputMessage.type.sizeof;
844 	im.type = type;
845 	auto len = socket.send((cast(ubyte*)&im)[0 .. im.eventLength]);
846 	if(len <= 0) {
847 		throw new Exception("wtf");
848 	}
849 }
850 
851 void handleEvent(Terminal* terminal, ref Session session, InputEvent event, Socket socket) {
852 	// FIXME: UI stuff
853 	static bool escaping;
854 	static bool gettingCommandLine;
855 	static bool gettingListSelection;
856 	static LineGetter lineGetter;
857 	
858 	InputMessage im;
859 	im.eventLength = im.sizeof;
860 	InputMessage* eventToSend;
861 
862 
863 	if(gettingCommandLine) {
864 		if(!lineGetter.workOnLine(event)) {
865 			gettingCommandLine = false;
866 			auto cmdLine = lineGetter.finishGettingLine();
867 
868 			import std..string;
869 			auto args = split(cmdLine, " ");
870 			if(args.length)
871 			switch(args[0]) {
872 				case "attach":
873 					attach(terminal, session, args.length > 1 ? args[1] : null);
874 				break;
875 				case "title":
876 					session.children[session.activeScreen].titlePrefix = join(args[1..$], " ");
877 				break;
878 				default:
879 			}
880 
881 			outputPaused = false;
882 			forceRedraw(terminal, session);
883 		}
884 
885 		return;
886 	}
887 
888 	if(gettingListSelection) {
889 		if(event.type == InputEvent.Type.CharacterEvent) {
890 			auto ce = event.get!(InputEvent.Type.CharacterEvent);
891 			if(ce.eventType == CharacterEvent.Type.Released)
892 				return;
893 
894 			switch(ce.character) {
895 				case '0':
896 				..
897 				case '9':
898 					int num = cast(int) (ce.character - '0');
899 					if(!session.zeroBasedCounting) {
900 						if(num == 0)
901 							num = 9;
902 						else
903 							num--;
904 					}
905 					setActiveScreen(terminal, session, num);
906 				goto case;
907 
908 				case '\n':
909 					gettingListSelection = false;
910 					outputPaused = false;
911 					forceRedraw(terminal, session);
912 					return;
913 				default:
914 			}
915 		}
916 	}
917 
918 	void triggerCommandLine(string text = "") {
919 		terminal.moveTo(0, terminal.height - 1);
920 		terminal.color(Color.DEFAULT, Color.DEFAULT, ForceOption.alwaysSend, true);
921 		terminal.write(":");
922 		foreach(i; 1 .. terminal.width)
923 			terminal.write(" ");
924 		terminal.moveTo(1, terminal.height - 1);
925 		gettingCommandLine = true;
926 		if(lineGetter is null)
927 			lineGetter = new LineGetter(terminal);
928 		lineGetter.startGettingLine();
929 		lineGetter.addString(text);
930 		outputPaused = true;
931 
932 		if(text.length)
933 			lineGetter.redraw();
934 	}
935 
936 	final switch(event.type) {
937 		case InputEvent.Type.EndOfFileEvent:
938 			// assert(0);
939 			// FIXME: is this right too?
940 			running = false;
941 		break;
942 		case InputEvent.Type.HangupEvent:
943 			running = false;
944 		break;
945 		case InputEvent.Type.KeyboardEvent:
946 			break; // FIXME: KeyboardEvent replaces CharacterEvent and NonCharacterKeyEvent
947 		case InputEvent.Type.CharacterEvent:
948 			auto ce = event.get!(InputEvent.Type.CharacterEvent);
949 			if(ce.eventType == CharacterEvent.Type.Released)
950 				return;
951 
952 			if(escaping) {
953 				// C-a C-a toggles active screens quickly
954 				if(session.escapeCharacter != dchar.init && ce.character == session.escapeCharacter) {
955 					if(previousScreen != session.activeScreen && previousScreen < session.children.length && session.children[previousScreen].socket !is null)
956 						setActiveScreen(terminal, session, previousScreen);
957 					else {
958 						setActiveScreen(terminal, session, nextScreen(session));
959 					}
960 					// C-a a sends C-a to the child.
961 				} else if(session.escapeCharacter != dchar.init && ce.character == session.escapeCharacter + 'a' - 1) {
962 					im.type = InputMessage.Type.CharacterPressed;
963 					im.characterEvent.character = 1;
964 					im.eventLength = im.characterEvent.offsetof + im.CharacterEvent.sizeof;
965 					eventToSend = &im;
966 				} else switch(ce.character) {
967 					case 'q': debugMode = !debugMode; break;
968 					case 't':
969 						session.showingTaskbar = !session.showingTaskbar;
970 						forceRedraw(terminal, session); // redraw full because the height changes
971 					break;
972 					case ' ':
973 						setActiveScreen(terminal, session, nextScreen(session));
974 					break;
975 					case 'd':
976 						// detach the session
977 						running = false;
978 					break;
979 					case 'D':
980 						// detach only the screen
981 						// closeSocket(terminal, session); //  don't really like this
982 					break;
983 					case 'i':
984 						// request information
985 						terminal.writeln(session.children[session.activeScreen].socketName);
986 					break;
987 					case 12: // ^L
988 						forceRedraw(terminal, session);
989 					break;
990 					case '"':
991 						// list everything and give ui to choose
992 						// FIXME: finish the UI of this
993 						terminal.clear();
994 						foreach(idx, child; session.children)
995 							terminal.writeln("\t", idx + 1, ": ", child.titlePrefix, child.title, " (", child.socketName, ".socket)");
996 						gettingListSelection = true;
997 						outputPaused = true;
998 					break;
999 					case ':':
1000 						triggerCommandLine();
1001 					break;
1002 					case 'c':
1003 						attach(terminal, session, null);
1004 						session.saveUpdatedSessionToFile();
1005 					break;
1006 					case 'C':
1007 						triggerCommandLine("attach ");
1008 					break;
1009 					case '0':
1010 					..
1011 					case '9':
1012 						int num = cast(int) (ce.character - '0');
1013 						if(!session.zeroBasedCounting) {
1014 							if(num == 0)
1015 								num = 9;
1016 							else
1017 								num--;
1018 						}
1019 						setActiveScreen(terminal, session, num);
1020 					break;
1021 					default:
1022 				}
1023 				escaping = false;
1024 			} else if(ce.character == session.escapeCharacter) {
1025 				escaping = true;
1026 			} else {
1027 				im.type = InputMessage.Type.CharacterPressed;
1028 				im.eventLength = im.characterEvent.offsetof + im.CharacterEvent.sizeof;
1029 				im.characterEvent.character = ce.character;
1030 				eventToSend = &im;
1031 			}
1032 		break;
1033 		case InputEvent.Type.SizeChangedEvent:
1034 			/*
1035 			auto ce = event.get!(InputEvent.Type.SizeChangedEvent);
1036 			im.type = InputMessage.Type.SizeChanged;
1037 			im.sizeEvent.width = ce.newWidth;
1038 			im.sizeEvent.height = ce.newHeight - (session.showingTaskbar ? 1 : 0);
1039 			eventToSend = &im;
1040 			*/
1041 			forceRedraw(terminal, session);  // the forced redraw will send the new size too
1042 		break;
1043 		case InputEvent.Type.UserInterruptionEvent:
1044 			im.type = InputMessage.Type.CharacterPressed;
1045 			im.characterEvent.character = '\003';
1046 			eventToSend = &im;
1047 		break;
1048 		case InputEvent.Type.NonCharacterKeyEvent:
1049 			auto ev = event.get!(InputEvent.Type.NonCharacterKeyEvent);
1050 			if(ev.eventType == NonCharacterKeyEvent.Type.Pressed) {
1051 
1052 				if(escaping) {
1053 					switch(ev.key) {
1054 						case NonCharacterKeyEvent.Key.LeftArrow:
1055 							if(ev.modifierState & ModifierState.alt) {
1056 								// alt + arrow will move the tab
1057 								if(session.activeScreen) {
1058 									auto c = session.children[session.activeScreen - 1];
1059 									session.children[session.activeScreen - 1] = session.children[session.activeScreen];
1060 									session.children[session.activeScreen] = c;
1061 
1062 									session.activeScreen--;
1063 								}
1064 								drawTaskbar(terminal, session);
1065 							} else
1066 								setActiveScreen(terminal, session, nextScreenBackwards(session));
1067 						break;
1068 						case NonCharacterKeyEvent.Key.RightArrow:
1069 							if(ev.modifierState & ModifierState.alt) {
1070 								// alt + arrow will move the tab
1071 								if(session.activeScreen + 1 < session.children.length) {
1072 									auto c = session.children[session.activeScreen + 1];
1073 									session.children[session.activeScreen + 1] = session.children[session.activeScreen];
1074 									session.children[session.activeScreen] = c;
1075 
1076 									session.activeScreen++;
1077 								}
1078 								drawTaskbar(terminal, session);
1079 							} else
1080 								setActiveScreen(terminal, session, nextScreen(session));
1081 						break;
1082 						default:
1083 					}
1084 					//escaping = false;
1085 					// staying in escape mode so you can cycle with arrows more easily. hit enter to go back to normal mode
1086 					return;
1087 				}
1088 
1089 				im.type = InputMessage.Type.KeyPressed;
1090 
1091 				im.keyEvent.key = cast(int) ev.key; // this can be casted to a TerminalKey later
1092 				im.keyEvent.modifiers = 0;
1093 				if(ev.modifierState & ModifierState.shift)
1094 					im.keyEvent.modifiers |= InputMessage.Shift;
1095 				if(ev.modifierState & ModifierState.control)
1096 					im.keyEvent.modifiers |= InputMessage.Ctrl;
1097 				if((ev.modifierState & ModifierState.alt) || (ev.modifierState & ModifierState.meta))
1098 					im.keyEvent.modifiers |= InputMessage.Alt;
1099 				eventToSend = &im;
1100 			}
1101 		break;
1102 		case InputEvent.Type.PasteEvent:
1103 			auto ev = event.get!(InputEvent.Type.PasteEvent);
1104 			auto data = new ubyte[](ev.pastedText.length + InputMessage.sizeof);
1105 			auto msg = cast(InputMessage*) data.ptr;
1106 			if(ev.pastedText.length > 4000)
1107 				break; // FIXME
1108 			msg.pasteEvent.pastedTextLength = cast(short) ev.pastedText.length;
1109 
1110 			//terminal.writeln(ev.pastedText);
1111 
1112 			// built-in array copy complained about byte overlap. Probably alignment or something.
1113 			foreach(i, b; ev.pastedText)
1114 				msg.pasteEvent.pastedText.ptr[i] = b;
1115 
1116 			msg.type = InputMessage.Type.DataPasted;
1117 			msg.eventLength = cast(short) data.length;
1118 			eventToSend = msg;
1119 		break;
1120 		case InputEvent.Type.MouseEvent:
1121 			auto me = event.get!(InputEvent.Type.MouseEvent);
1122 
1123 			if(session.showingTaskbar && me.y == terminal.height - 1) {
1124 				if(me.eventType == MouseEvent.Type.Pressed)
1125 					foreach(idx, child; session.children) {
1126 						if(me.x >= child.x && me.x < child.x2) {
1127 							setActiveScreen(terminal, session, cast(int) idx);
1128 							break;
1129 						}
1130 					}
1131 				return;
1132 			}
1133 
1134 			final switch(me.eventType) {
1135 				case MouseEvent.Type.Moved:
1136 					im.type = InputMessage.Type.MouseMoved;
1137 					if(!session.mouseTrackingOn && me.buttons == 0)
1138 						return;
1139 				break;
1140 				case MouseEvent.Type.Pressed:
1141 					im.type = InputMessage.Type.MousePressed;
1142 				break;
1143 				case MouseEvent.Type.Released:
1144 					im.type = InputMessage.Type.MouseReleased;
1145 				break;
1146 				case MouseEvent.Type.Clicked:
1147 					// FIXME
1148 			}
1149 
1150 			eventToSend = &im;
1151 
1152 			im.mouseEvent.x = cast(short) me.x;
1153 			im.mouseEvent.y = cast(short) me.y;
1154 			im.mouseEvent.button = cast(ubyte) me.buttons;
1155 			im.mouseEvent.modifiers = 0;
1156 			if(me.modifierState & ModifierState.shift)
1157 				im.mouseEvent.modifiers |= InputMessage.Shift;
1158 			if(me.modifierState & ModifierState.control)
1159 				im.mouseEvent.modifiers |= InputMessage.Ctrl;
1160 			if(me.modifierState & ModifierState.alt || me.modifierState & ModifierState.meta)
1161 				im.mouseEvent.modifiers |= InputMessage.Alt;
1162 		break;
1163 		case InputEvent.Type.CustomEvent:
1164 		break;
1165 	}
1166 
1167 	if(eventToSend !is null && socket !is null) {
1168 		import core.sys.posix.unistd;
1169 		auto len = write(socket.handle, eventToSend, eventToSend.eventLength);
1170 		if(len <= 0) {
1171 			closeSocket(terminal, session);
1172 		}
1173 	}
1174 }
1175 
1176 void sendSimulatedInput(Socket socket, string input) {
1177 	if(input.length == 0) return;
1178 	if(socket is null) return;
1179 
1180 	auto data = new ubyte[](input.length + InputMessage.sizeof);
1181 	auto msg = cast(InputMessage*) data.ptr;
1182 
1183 	msg.pasteEvent.pastedTextLength = cast(short) input.length;
1184 
1185 	// built-in array copy complained about byte overlap. Probably alignment or something.
1186 	foreach(i, b; input)
1187 		msg.pasteEvent.pastedText.ptr[i] = b;
1188 
1189 	msg.type = InputMessage.Type.DataPasted;
1190 	msg.eventLength = cast(short) data.length;
1191 
1192 
1193 	import core.sys.posix.unistd;
1194 	auto len = write(socket.handle, msg, msg.eventLength);
1195 }
1196 
1197 void forceRedraw(Terminal* terminal, ref Session session) {
1198 	assert(!outputPaused);
1199 	setActiveScreen(terminal, session, session.activeScreen, true);
1200 }
1201 
1202 void setActiveScreen(Terminal* terminal, ref Session session, int s, bool force = false) {
1203 	if(s < 0 || s >= session.children.length)
1204 		return;
1205 	if(session.activeScreen == s && !force)
1206 		return; // already active
1207 	if(session.children[s].socket is null)
1208 		return; // vacant slot cannot be activated
1209 
1210 	if(previousScreen != session.activeScreen)
1211 		previousScreen = session.activeScreen;
1212 	session.activeScreen = s;
1213 
1214 	session.children[s].demandsAttention = false;
1215 
1216 	terminal.clear();
1217 
1218 	socket = session.children[s].socket;
1219 
1220 	drawTaskbar(terminal, session);
1221 
1222 	// force the size
1223 	{
1224 		InputMessage im;
1225 		im.eventLength = im.sizeof;
1226 		im.sizeEvent.width = cast(short) terminal.width;
1227 		im.sizeEvent.height = cast(short) (terminal.height - (session.showingTaskbar ? 1 : 0));
1228 		im.type = InputMessage.Type.SizeChanged;
1229 		import core.sys.posix.unistd;
1230 		write(socket.handle, &im, im.eventLength);
1231 	}
1232 
1233 	// and force a redraw
1234 	{
1235 		InputMessage im;
1236 		im.eventLength = im.sizeof;
1237 		im.type = InputMessage.Type.RedrawNow;
1238 		import core.sys.posix.unistd;
1239 		write(socket.handle, &im, im.eventLength);
1240 	}
1241 }
1242 
1243 void closeSocket(Terminal* terminal, ref Session session, Socket socketToClose = null) {
1244 	if(socketToClose is null)
1245 		socketToClose = socket;
1246 	assert(socketToClose !is null);
1247 
1248 	int switchTo = -1;
1249 	foreach(idx, ref child; session.children) {
1250 		if(child.socket is socketToClose) {
1251 			if(idx == session.children.length - 1) {
1252 				session.children = session.children[0 .. $-1];
1253 				while(session.children.length && session.children[$-1].socket is null)
1254 					session.children = session.children[0 .. $-1];
1255 			} else {
1256 				child.socket = null;
1257 				child.title = "[vacant]";
1258 			}
1259 			switchTo = previousScreen;
1260 			break;
1261 		}
1262 	}
1263 
1264 	socketToClose.shutdown(SocketShutdown.BOTH);
1265 	socketToClose.close();
1266 
1267 	if(socketToClose !is socket) {
1268 		drawTaskbar(terminal, session);
1269 		return; // no need to close; it isn't the active socket
1270 	}
1271 
1272 	socket = null;
1273 
1274 	if(switchTo >= session.children.length)
1275 		switchTo = 0;
1276 
1277 	foreach(s; switchTo .. session.children.length)
1278 		if(session.children[s].socket !is null) {
1279 			switchTo = cast(int) s;
1280 			goto allSet;
1281 		}
1282 	foreach(s; 0 .. switchTo)
1283 		if(session.children[s].socket !is null) {
1284 			switchTo = cast(int) s;
1285 			goto allSet;
1286 		}
1287 
1288 	switchTo = -1;
1289 
1290 	allSet:
1291 
1292 	if(switchTo < 0 || switchTo >= session.children.length) {
1293 		running = false;
1294 		socket = null;
1295 		return;
1296 	} else if(session.children[switchTo].socket is null) {
1297 		running = false;
1298 		socket = null;
1299 		return;
1300 	}
1301 	setActiveScreen(terminal, session, switchTo, true);
1302 }
1303 
1304