xref: /haiku/src/apps/terminal/TermApp.cpp (revision 582da17386c4a192ca30270d6b0b95f561cf5843)
1 /*
2  * Copyright 2001-2007, Haiku.
3  * Copyright (c) 2003-2004 Kian Duffy <myob@users.sourceforge.net>
4  * Copyright (C) 1998,99 Kazuho Okui and Takashi Murai.
5  *
6  * Distributed unter the terms of the MIT license.
7  */
8 
9 
10 #include "TermApp.h"
11 
12 #include "CodeConv.h"
13 #include "PrefHandler.h"
14 #include "TermBuffer.h"
15 #include "TermWindow.h"
16 #include "TermConst.h"
17 
18 #include <Alert.h>
19 #include <Clipboard.h>
20 #include <NodeInfo.h>
21 #include <Path.h>
22 #include <Roster.h>
23 #include <Screen.h>
24 #include <String.h>
25 #include <TextView.h>
26 
27 #include <stdio.h>
28 #include <stdlib.h>
29 
30 
31 static bool sUsageRequested = false;
32 static bool sGeometryRequested = false;
33 
34 struct standard_args {
35 	char *name;
36 	char *longname;
37 	int priority;
38 	int nargs;
39 	const char *prefname;
40 };
41 
42 struct standard_args standard_args[] = {
43 	{ "-h", "--help", 90, 0, NULL },
44 	{ "-f", "--fullscreen", 30, 0, NULL },
45 	{ "-p", "--preference", 80, 1, NULL },
46 	{ "-t", "--title", 70, 1, NULL },
47 	{ "-geom", "--geometry", 50, 1, NULL },
48 };
49 
50 int argmatch(char **, int, char *, char *, int, char **, int *);
51 void sort_args(int, char **);
52 
53 
54 const ulong MSG_ACTIVATE_TERM = 'msat';
55 const ulong MSG_TERM_IS_MINIMIZE = 'mtim';
56 
57 
58 TermApp::TermApp()
59 	: BApplication(TERM_SIGNATURE),
60 	fStartFullscreen(false),
61 	fWindowNumber(-1),
62 	fTermWindow(NULL)
63 {
64 	fWindowTitle = "Terminal";
65 	_RegisterTerminal();
66 
67 	if (fWindowNumber > 0)
68 		fWindowTitle << " " << fWindowNumber;
69 
70 	int i = fWindowNumber / 16;
71 	int j = fWindowNumber % 16;
72 	int k = (j * 16) + (i * 64) + 50;
73 	int l = (j * 16)  + 50;
74 
75 	fTermFrame.Set(k, l, k + 50, k + 50);
76 }
77 
78 
79 TermApp::~TermApp()
80 {
81 }
82 
83 
84 void
85 TermApp::ReadyToRun()
86 {
87 	// Prevent opeing window when option -h is given.
88 	if (sUsageRequested)
89 		return;
90 
91 	status_t status = _MakeTermWindow(fTermFrame);
92 
93 	// failed spawn, print stdout and open alert panel
94 	// TODO: This alert does never show up.
95 	if (status < B_OK) {
96 		(new BAlert("alert", "Terminal couldn't start the shell. Sorry.",
97 			"ok", NULL, NULL, B_WIDTH_FROM_LABEL,
98 			B_INFO_ALERT))->Go(NULL);
99 		PostMessage(B_QUIT_REQUESTED);
100 		return;
101 	}
102 
103 	// using BScreen::Frame isn't enough
104 	if (fStartFullscreen)
105 		BMessenger(fTermWindow).SendMessage(FULLSCREEN);
106 }
107 
108 
109 void
110 TermApp::Quit()
111 {
112 	if (!sUsageRequested)
113 		_UnregisterTerminal();
114 
115 	BApplication::Quit();
116 }
117 
118 
119 void
120 TermApp::AboutRequested()
121 {
122 	BAlert *alert = new BAlert("about", "Terminal\n"
123 		"\twritten by Kazuho Okui and Takashi Murai\n"
124 		"\tupdated by Kian Duffy and others\n\n"
125 		"\tCopyright " B_UTF8_COPYRIGHT "2003-2005, Haiku.\n", "Ok");
126 	BTextView *view = alert->TextView();
127 
128 	view->SetStylable(true);
129 
130 	BFont font;
131 	view->GetFont(&font);
132 	font.SetSize(18);
133 	font.SetFace(B_BOLD_FACE);
134 	view->SetFontAndColor(0, 8, &font);
135 
136 	alert->Go();
137 }
138 
139 
140 void
141 TermApp::MessageReceived(BMessage* msg)
142 {
143 	switch (msg->what) {
144 		case MENU_SWITCH_TERM:
145 			_SwitchTerm();
146 			break;
147 
148 		case MSG_ACTIVATE_TERM:
149 			fTermWindow->TermWinActivate();
150 			break;
151 
152 		case MSG_TERM_IS_MINIMIZE:
153 		{
154 			BMessage reply(B_REPLY);
155 			reply.AddBool("result", fTermWindow->IsMinimized());
156 			msg->SendReply(&reply);
157 			break;
158 		}
159 
160 		default:
161 			BApplication::MessageReceived(msg);
162 			break;
163 	}
164 }
165 
166 
167 void
168 TermApp::ArgvReceived(int32 argc, char **argv)
169 {
170 	int skip_args = 0;
171 	char *value = 0;
172 
173 	if (argc < 2)
174 		return;
175 
176 	sort_args(argc, argv);
177 	argc = 0;
178 	while (argv[argc])
179 		argc++;
180 
181 	// Print usage
182 	if (argmatch(argv, argc, "-help", "--help", 3, NULL, &skip_args)) {
183 		_Usage(argv[0]);
184 		sUsageRequested = true;
185 		PostMessage(B_QUIT_REQUESTED);
186 	}
187 
188 	// Start fullscreen
189 	if (argmatch(argv, argc, "-f", "--fullscreen", 4, NULL, &skip_args))
190 		fStartFullscreen = true;
191 
192 	// Load preference file
193 	if (argmatch(argv, argc, "-p", "--preference", 4, &value, &skip_args))
194 		PrefHandler::Default()->Open(value);
195 
196 	// Set window title
197 	if (argmatch(argv ,argc, "-t", "--title", 3, &value, &skip_args))
198 		fWindowTitle = value;
199 
200     	// Set window geometry
201     	if (argmatch(argv, argc, "-geom", "--geometry", 4, &value, &skip_args)) {
202 		int width, height, xpos, ypos;
203 
204 		sscanf(value, "%dx%d+%d+%d", &width, &height, &xpos, &ypos);
205 		if (width < 0 || height < 0 || xpos < 0 || ypos < 0
206 			|| width >= 256 || height >= 256 || xpos >= 2048 || ypos >= 2048) {
207 			fprintf(stderr, "%s: invalid geometry format or value.\n", argv[0]);
208 			fprintf(stderr, "Try `%s --help' for more information.\n", argv[0]);
209 			sUsageRequested = true;
210 			PostMessage(B_QUIT_REQUESTED);
211 		}
212 		PrefHandler::Default()->setInt32(PREF_COLS, width);
213 		PrefHandler::Default()->setInt32(PREF_ROWS, height);
214 
215 		fTermFrame.Set(xpos, ypos, xpos + 50, ypos + 50);
216 		sGeometryRequested = true;
217    	 }
218 
219 	skip_args++;
220 
221 	if (skip_args < argc) {
222 		// Check invalid options
223 
224 		if (*argv[skip_args] == '-') {
225 			fprintf(stderr, "%s: invalid option `%s'\n", argv[0], argv[skip_args]);
226 			fprintf(stderr, "Try `%s --help' for more information.\n", argv[0]);
227 			sUsageRequested = true;
228 			PostMessage(B_QUIT_REQUESTED);
229 		}
230 
231 		fCommandLine += argv[skip_args++];
232 		while (skip_args < argc) {
233 			fCommandLine += ' ';
234 			fCommandLine += argv[skip_args++];
235 		}
236 	}
237 }
238 
239 
240 void
241 TermApp::RefsReceived(BMessage* message)
242 {
243 	// Works Only Launced by Double-Click file, or Drags file to App.
244 	if (!IsLaunching())
245 		return;
246 
247 	entry_ref ref;
248 	if (message->FindRef("refs", 0, &ref) != B_OK)
249 		return;
250 
251 	BFile file;
252 	if (file.SetTo(&ref, B_READ_WRITE) != B_OK)
253 		return;
254 
255 	BNodeInfo info(&file);
256 	char mimetype[B_MIME_TYPE_LENGTH];
257 	info.GetType(mimetype);
258 
259 	// if App opened by Pref file
260 	if (!strcmp(mimetype, PREFFILE_MIMETYPE)) {
261 
262 		BEntry ent(&ref);
263 		BPath path(&ent);
264 		PrefHandler::Default()->OpenText(path.Path());
265 		return;
266 	}
267 
268 	// if App opened by Shell Script
269 	if (!strcmp(mimetype, "text/x-haiku-shscript")){
270 		// Not implemented.
271 		//    beep();
272 		return;
273 	}
274 }
275 
276 
277 status_t
278 TermApp::_MakeTermWindow(BRect &frame)
279 {
280 	const char *command = NULL;
281 	if (fCommandLine.Length() > 0)
282 		command = fCommandLine.String();
283 	else
284 		command = PrefHandler::Default()->getString(PREF_SHELL);
285 
286 	try {
287 		fTermWindow = new TermWindow(frame, fWindowTitle.String(), command);
288 	} catch (int error) {
289 		return (status_t)error;
290 	} catch (...) {
291 		return B_ERROR;
292 	}
293 
294 	fTermWindow->Show();
295 
296 	return B_OK;
297 }
298 
299 
300 void
301 TermApp::_ActivateTermWindow(team_id id)
302 {
303 	BMessenger app(TERM_SIGNATURE, id);
304 	if (app.IsTargetLocal())
305 		fTermWindow->Activate();
306 	else
307 		app.SendMessage(MSG_ACTIVATE_TERM);
308 }
309 
310 
311 void
312 TermApp::_SwitchTerm()
313 {
314 	team_id myId = be_app->Team(); // My id
315 	BList teams;
316 	be_roster->GetAppList(TERM_SIGNATURE, &teams);
317 	int32 numTerms = teams.CountItems();
318 
319 	if (numTerms <= 1 )
320 		return; //Can't Switch !!
321 
322 	// Find position of mine in app teams.
323 	int32 i;
324 
325 	for (i = 0; i < numTerms; i++) {
326 		if (myId == reinterpret_cast<team_id>(teams.ItemAt(i)))
327 			break;
328 	}
329 
330 	do {
331 		if (--i < 0)
332 			i = numTerms - 1;
333 	} while (_IsMinimized(reinterpret_cast<team_id>(teams.ItemAt(i))));
334 
335 	// Activate switched terminal.
336 	_ActivateTermWindow(reinterpret_cast<team_id>(teams.ItemAt(i)));
337 }
338 
339 
340 bool
341 TermApp::_IsMinimized(team_id id)
342 {
343 	BMessenger app(TERM_SIGNATURE, id);
344 	if (app.IsTargetLocal())
345 		return fTermWindow->IsMinimized();
346 
347 	BMessage reply;
348 	if (app.SendMessage(MSG_TERM_IS_MINIMIZE, &reply) != B_OK)
349 		return true;
350 
351 	bool hidden;
352 	reply.FindBool("result", &hidden);
353 	return hidden;
354 }
355 
356 
357 /*!
358 	Checks if all teams that have an ID-to-team mapping in the message
359 	are still running.
360 	The IDs for teams that are gone will be made available again, and
361 	their mapping is removed from the message.
362 */
363 void
364 TermApp::_SanitizeIDs(BMessage* data, uint8* windows, ssize_t length)
365 {
366 	BList teams;
367 	be_roster->GetAppList(TERM_SIGNATURE, &teams);
368 
369 	for (int32 i = 0; i < length; i++) {
370 		if (!windows[i])
371 			continue;
372 
373 		BString id("id-");
374 		id << i;
375 
376 		team_id team;
377 		if (data->FindInt32(id.String(), &team) != B_OK)
378 			continue;
379 
380 		if (!teams.HasItem((void*)team)) {
381 			windows[i] = false;
382 			data->RemoveName(id.String());
383 		}
384 	}
385 }
386 
387 
388 /*!
389 	Removes the current fWindowNumber (ID) from the supplied array, or
390 	finds a free ID in it, and sets fWindowNumber accordingly.
391 */
392 bool
393 TermApp::_UpdateIDs(bool set, uint8* windows, ssize_t maxLength,
394 	ssize_t* _length)
395 {
396 	ssize_t length = *_length;
397 
398 	if (set) {
399 		int32 i;
400 		for (i = 0; i < length; i++) {
401 			if (!windows[i]) {
402 				windows[i] = true;
403 				fWindowNumber = i + 1;
404 				break;
405 			}
406 		}
407 
408 		if (i == length) {
409 			if (length >= maxLength)
410 				return false;
411 
412 			windows[length] = true;
413 			length++;
414 			fWindowNumber = length;
415 		}
416 	} else {
417 		// update information and write it back
418 		windows[fWindowNumber - 1] = false;
419 	}
420 
421 	*_length = length;
422 	return true;
423 }
424 
425 
426 void
427 TermApp::_UpdateRegistration(bool set)
428 {
429 	if (set)
430 		fWindowNumber = -1;
431 	else if (fWindowNumber < 0)
432 		return;
433 
434 #ifdef __HAIKU__
435 	// use BClipboard - it supports atomic access in Haiku
436 	BClipboard clipboard(TERM_SIGNATURE);
437 
438 	while (true) {
439 		if (!clipboard.Lock())
440 			return;
441 
442 		BMessage* data = clipboard.Data();
443 
444 		const uint8* windowsData;
445 		uint8 windows[512];
446 		ssize_t length;
447 		if (data->FindData("ids", B_RAW_TYPE,
448 				(const void**)&windowsData, &length) != B_OK)
449 			length = 0;
450 
451 		if (length > (ssize_t)sizeof(windows))
452 			length = sizeof(windows);
453 		if (length > 0)
454 			memcpy(windows, windowsData, length);
455 
456 		_SanitizeIDs(data, windows, length);
457 
458 		status_t status = B_OK;
459 		if (_UpdateIDs(set, windows, sizeof(windows), &length)) {
460 			// add/remove our ID-to-team mapping
461 			BString id("id-");
462 			id << fWindowNumber;
463 
464 			if (set)
465 				data->AddInt32(id.String(), Team());
466 			else
467 				data->RemoveName(id.String());
468 
469 			data->RemoveName("ids");
470 			//if (data->ReplaceData("ids", B_RAW_TYPE, windows, length) != B_OK)
471 			data->AddData("ids", B_RAW_TYPE, windows, length);
472 
473 			status = clipboard.Commit(true);
474 		}
475 
476 		clipboard.Unlock();
477 
478 		if (status == B_OK)
479 			break;
480 	}
481 #else	// !__HAIKU__
482 	// use a file to store the IDs - unfortunately, locking
483 	// doesn't work on BeOS either here
484 	int fd = open("/tmp/terminal_ids", O_RDWR | O_CREAT);
485 	if (fd < 0)
486 		return;
487 
488 	struct flock lock;
489 	lock.l_type = F_WRLCK;
490 	lock.l_whence = SEEK_CUR;
491 	lock.l_start = 0;
492 	lock.l_len = -1;
493 	fcntl(fd, F_SETLKW, &lock);
494 
495 	uint8 windows[512];
496 	ssize_t length = read_pos(fd, 0, windows, sizeof(windows));
497 	if (length < 0) {
498 		close(fd);
499 		return;
500 	}
501 
502 	if (length > (ssize_t)sizeof(windows))
503 		length = sizeof(windows);
504 
505 	if (_UpdateIDs(set, windows, sizeof(windows), &length))
506 		write_pos(fd, 0, windows, length);
507 
508 	close(fd);
509 #endif	// !__HAIKU__
510 }
511 
512 
513 void
514 TermApp::_UnregisterTerminal()
515 {
516 	_UpdateRegistration(false);
517 }
518 
519 
520 void
521 TermApp::_RegisterTerminal()
522 {
523 	_UpdateRegistration(true);
524 }
525 
526 
527 //#ifndef B_NETPOSITIVE_APP_SIGNATURE
528 //#define B_NETPOSITIVE_APP_SIGNATURE "application/x-vnd.Be-NPOS"
529 //#endif
530 //
531 //void
532 //TermApp::ShowHTML(BMessage *msg)
533 //{
534 //  const char *url;
535 //  msg->FindString("Url", &url);
536 //  BMessage message;
537 //
538 //  message.what = B_NETPOSITIVE_OPEN_URL;
539 //  message.AddString("be:url", url);
540 
541 //  be_roster->Launch(B_NETPOSITIVE_APP_SIGNATURE, &message);
542 //  while(!(be_roster->IsRunning(B_NETPOSITIVE_APP_SIGNATURE)))
543 //    snooze(10000);
544 //
545 //  // Activate net+
546 //  be_roster->ActivateApp(be_roster->TeamFor(B_NETPOSITIVE_APP_SIGNATURE));
547 //}
548 
549 
550 void
551 TermApp::_Usage(char *name)
552 {
553 	fprintf(stderr, "Haiku Terminal\n"
554 		"Copyright 2001-2007 Haiku, Inc.\n"
555 		"Copyright(C) 1999 Kazuho Okui and Takashi Murai.\n"
556 		"\n"
557 		"Usage: %s [OPTION] [SHELL]\n", name);
558 
559 	fprintf(stderr, "  -p,     --preference         load preference file\n"
560 		"  -t,     --title              set window title\n"
561 		"  -geom,  --geometry           set window geometry\n"
562 		"                               An example of geometry is \"80x25+100+100\"\n");
563 }
564 
565 
566 
567 // This routine copy from GNU Emacs.
568 // TODO: This might be a GPL licensing issue here. Investigate.
569 int
570 argmatch(char **argv, int argc, char *sstr, char *lstr,
571 	int minlen, char **valptr, int *skipptr)
572 {
573 	char *p = 0;
574 	int arglen;
575 	char *arg;
576 
577 	// Don't access argv[argc]; give up in advance
578 	if (argc <= *skipptr + 1)
579 		return 0;
580 
581 	arg = argv[*skipptr+1];
582 	if (arg == NULL)
583 		return 0;
584 
585 	if (strcmp(arg, sstr) == 0) {
586 		if(valptr != NULL) {
587 			*valptr = argv[*skipptr+2];
588 			*skipptr += 2;
589 		} else
590 			*skipptr += 1;
591 		return 1;
592 	}
593 
594 	arglen =(valptr != NULL &&(p = strchr(arg, '=')) != NULL
595 			? p - arg : strlen(arg));
596 
597 	if(lstr == 0 || arglen < minlen || strncmp(arg, lstr, arglen) != 0)
598 		return 0;
599 	else
600 	if(valptr == NULL)
601 	{
602 		*skipptr += 1;
603 		return 1;
604 	}
605 	else
606 	if(p != NULL)
607 	{
608 		*valptr = p+1;
609 		*skipptr += 1;
610 		return 1;
611 	}
612 	else
613 	if(argv[*skipptr+2] != NULL)
614 	{
615 		*valptr = argv[*skipptr+2];
616 		*skipptr += 2;
617 		return 1;
618 	}
619 	else
620 	{
621 		return 0;
622 	}
623 }
624 
625 // This routine copy from GNU Emacs.
626 // TODO: This might be a GPL licensing issue here. Investigate.
627 void
628 sort_args(int argc, char **argv)
629 {
630 	/*
631 		For each element of argv,
632 		the corresponding element of options is:
633 		0 for an option that takes no arguments,
634 		1 for an option that takes one argument, etc.
635 		-1 for an ordinary non-option argument.
636 	*/
637 
638 	char **newargv =(char **) malloc(sizeof(char *) * argc);
639 
640 	int *options =(int *) malloc(sizeof(int) * argc);
641 	int *priority =(int *) malloc(sizeof(int) * argc);
642 	int to = 1;
643 	int incoming_used = 1;
644 	int from;
645 	int i;
646 	//int end_of_options = argc;
647 
648 	// Categorize all the options,
649 	// and figure out which argv elts are option arguments
650 	for(from = 1; from < argc; from++)
651 	{
652 		options[from] = -1;
653 		priority[from] = 0;
654 		if(argv[from][0] == '-')
655 		{
656 			int match, thislen;
657 			char *equals;
658 
659 			// If we have found "--", don't consider any more arguments as options
660 			if(argv[from][1] == '-' && argv[from][2] == 0)
661 			{
662 				// Leave the "--", and everything following it, at the end.
663 				for(; from < argc; from++)
664 				{
665 					priority[from] = -100;
666 					options[from] = -1;
667 				}
668 				break;
669 			}
670 
671 			// Look for a match with a known old-fashioned option.
672 			for(i = 0; i <(int)(sizeof(standard_args) / sizeof(standard_args[0])); i++)
673 				if(!strcmp(argv[from], standard_args[i].name))
674 				{
675 					options[from] = standard_args[i].nargs;
676 					priority[from] = standard_args[i].priority;
677 					if(from + standard_args[i].nargs >= argc)
678 						fprintf(stderr, "Option `%s' requires an argument\n", argv[from]);
679 					from += standard_args[i].nargs;
680 					goto done;
681 				}
682 
683 			/*
684 				Look for a match with a known long option.
685 				MATCH is -1 if no match so far, -2 if two or more matches so far,
686 				>= 0(the table index of the match) if just one match so far.
687 			*/
688 			if(argv[from][1] == '-')
689 			{
690 				match = -1;
691 				thislen = strlen(argv[from]);
692 				equals = strchr(argv[from], '=');
693 				if(equals != 0)
694 					thislen = equals - argv[from];
695 
696 				for(i = 0;i <(int )(sizeof(standard_args) / sizeof(standard_args[0])); i++)
697 					if(standard_args[i].longname
698 						&& !strncmp(argv[from], standard_args[i].longname, thislen))
699 					{
700 						if(match == -1)
701 							match = i;
702 						else
703 							match = -2;
704 					}
705 
706 					// If we found exactly one match, use that
707 					if(match >= 0)
708 					{
709 						options[from] = standard_args[match].nargs;
710 						priority[from] = standard_args[match].priority;
711 
712 						// If --OPTION=VALUE syntax is used,
713 						// this option uses just one argv element
714 						if(equals != 0)
715 							options[from] = 0;
716 						if(from + options[from] >= argc)
717 							fprintf(stderr, "Option `%s' requires an argument\n", argv[from]);
718 						from += options[from];
719 					}
720 			}
721 			done: ;
722 		}
723 	}
724 
725 	// Copy the arguments, in order of decreasing priority, to NEW
726 	newargv[0] = argv[0];
727 	while (incoming_used < argc) {
728 		int best = -1;
729 		int best_priority = -9999;
730 
731 		// Find the highest priority remaining option.
732 		// If several have equal priority, take the first of them.
733 		for (from = 1; from < argc; from++) {
734 			if (argv[from] != 0 && priority[from] > best_priority) {
735 				best_priority = priority[from];
736 				best = from;
737 			}
738 
739 			// Skip option arguments--they are tied to the options.
740 			if (options[from] > 0)
741 				from += options[from];
742 		}
743 
744 		if (best < 0)
745 			abort();
746 
747 		// Copy the highest priority remaining option, with its args, to NEW.
748 		// Unless it is a duplicate of the previous one
749 		if (!(options[best] == 0 && ! strcmp(newargv[to - 1], argv[best]))) {
750 			newargv[to++] = argv[best];
751 			for(i = 0; i < options[best]; i++)
752 				newargv[to++] = argv[best + i + 1];
753 		}
754 
755 		incoming_used += 1 +(options[best] > 0 ? options[best] : 0);
756 
757 		// Clear out this option in ARGV
758 		argv[best] = 0;
759 		for (i = 0; i < options[best]; i++)
760 			argv[best + i + 1] = 0;
761 	}
762 
763 	// If duplicate options were deleted, fill up extra space with null ptrs
764 	while (to < argc)
765 		newargv[to++] = 0;
766 
767 	memcpy(argv, newargv, sizeof(char *) * argc);
768 
769 	free(options);
770 	free(newargv);
771 	free(priority);
772 }
773