xref: /haiku/src/apps/terminal/TermApp.cpp (revision e0ef64750f3169cd634bb2f7a001e22488b05231)
1 /*
2  * Copyright 2001-2009, Haiku, Inc. All rights reserved.
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 <errno.h>
13 #include <signal.h>
14 #include <stdio.h>
15 #include <stdlib.h>
16 #include <unistd.h>
17 
18 #include <Alert.h>
19 #include <Catalog.h>
20 #include <Clipboard.h>
21 #include <Catalog.h>
22 #include <FindDirectory.h>
23 #include <InterfaceDefs.h>
24 #include <Locale.h>
25 #include <NodeInfo.h>
26 #include <Path.h>
27 #include <Roster.h>
28 #include <Screen.h>
29 #include <String.h>
30 
31 
32 #include "Arguments.h"
33 #include "Globals.h"
34 #include "PrefHandler.h"
35 #include "TermConst.h"
36 #include "TermView.h"
37 #include "TermWindow.h"
38 
39 
40 static bool sUsageRequested = false;
41 //static bool sGeometryRequested = false;
42 
43 const ulong MSG_ACTIVATE_TERM = 'msat';
44 const ulong MSG_TERM_WINDOW_INFO = 'mtwi';
45 
46 
47 int
48 main()
49 {
50 	TermApp app;
51 	app.Run();
52 
53 	return 0;
54 }
55 
56 #undef B_TRANSLATE_CONTEXT
57 #define B_TRANSLATE_CONTEXT "Terminal TermApp"
58 
59 TermApp::TermApp()
60 	: BApplication(TERM_SIGNATURE),
61 	fStartFullscreen(false),
62 	fWindowNumber(-1),
63 	fTermWindow(NULL),
64 	fArgs(NULL)
65 {
66 
67 	fArgs = new Arguments(0, NULL);
68 
69 	fWindowTitle = B_TRANSLATE("Terminal");
70 	_RegisterTerminal();
71 
72 	if (fWindowNumber > 0)
73 		fWindowTitle << " " << fWindowNumber;
74 
75 	if (_LoadWindowPosition(&fTermFrame, &fTermWorkspaces) != B_OK) {
76 		int i = fWindowNumber / 16;
77 		int j = fWindowNumber % 16;
78 		int k = (j * 16) + (i * 64) + 50;
79 		int l = (j * 16)  + 50;
80 
81 		fTermFrame.Set(k, l, k + 50, k + 50);
82 	}
83 }
84 
85 
86 TermApp::~TermApp()
87 {
88 	delete fArgs;
89 }
90 
91 
92 void
93 TermApp::ReadyToRun()
94 {
95 	// Prevent opeing window when option -h is given.
96 	if (sUsageRequested)
97 		return;
98 
99 	// Install a SIGCHLD signal handler, so that we will be notified, when
100 	// a shell exits.
101 	struct sigaction action;
102 #ifdef __HAIKU__
103 	action.sa_handler = (sighandler_t)_SigChildHandler;
104 #else
105 	action.sa_handler = (__signal_func_ptr)_SigChildHandler;
106 #endif
107 	sigemptyset(&action.sa_mask);
108 #ifdef SA_NODEFER
109 	action.sa_flags = SA_NODEFER;
110 #endif
111 	action.sa_userdata = this;
112 	if (sigaction(SIGCHLD, &action, NULL) < 0) {
113 		fprintf(stderr, B_TRANSLATE("sigaction() failed: %s\n"),
114 			strerror(errno));
115 		// continue anyway
116 	}
117 
118 	// init the mouse copy'n'paste clipboard
119 	gMouseClipboard = new BClipboard(MOUSE_CLIPBOARD_NAME, true);
120 
121 	status_t status = _MakeTermWindow(fTermFrame, fTermWorkspaces);
122 
123 	// failed spawn, print stdout and open alert panel
124 	// TODO: This alert does never show up.
125 	if (status < B_OK) {
126 		(new BAlert("alert",
127 			B_TRANSLATE("Terminal couldn't start the shell. Sorry."),
128 			B_TRANSLATE("OK"), NULL, NULL, B_WIDTH_FROM_LABEL,
129 			B_INFO_ALERT))->Go(NULL);
130 		PostMessage(B_QUIT_REQUESTED);
131 		return;
132 	}
133 
134 	// using BScreen::Frame isn't enough
135 	if (fStartFullscreen)
136 		BMessenger(fTermWindow).SendMessage(FULLSCREEN);
137 }
138 
139 
140 void
141 TermApp::Quit()
142 {
143 	if (!sUsageRequested)
144 		_UnregisterTerminal();
145 
146 	BApplication::Quit();
147 }
148 
149 
150 void
151 TermApp::AboutRequested()
152 {
153 	TermView::AboutRequested();
154 }
155 
156 
157 void
158 TermApp::MessageReceived(BMessage* msg)
159 {
160 	switch (msg->what) {
161 		case MENU_SWITCH_TERM:
162 			_SwitchTerm();
163 			break;
164 
165 		case MSG_ACTIVATE_TERM:
166 			fTermWindow->Activate();
167 			break;
168 
169 		case MSG_TERM_WINDOW_INFO:
170 		{
171 			BMessage reply(B_REPLY);
172 			reply.AddBool("minimized", fTermWindow->IsMinimized());
173 			reply.AddInt32("workspaces", fTermWindow->Workspaces());
174 			msg->SendReply(&reply);
175 			break;
176 		}
177 
178 		case MSG_SAVE_WINDOW_POSITION:
179 			_SaveWindowPosition(msg);
180 			break;
181 
182 		case MSG_CHECK_CHILDREN:
183 			_HandleChildCleanup();
184 			break;
185 
186 		default:
187 			BApplication::MessageReceived(msg);
188 			break;
189 	}
190 }
191 
192 
193 void
194 TermApp::ArgvReceived(int32 argc, char **argv)
195 {
196 	fArgs->Parse(argc, argv);
197 
198 	if (fArgs->UsageRequested()) {
199 		_Usage(argv[0]);
200 		sUsageRequested = true;
201 		PostMessage(B_QUIT_REQUESTED);
202 		return;
203 	}
204 
205 	if (fArgs->Title() != NULL)
206 		fWindowTitle = fArgs->Title();
207 
208 	fStartFullscreen = fArgs->FullScreen();
209 }
210 
211 
212 void
213 TermApp::RefsReceived(BMessage* message)
214 {
215 	// Works Only Launced by Double-Click file, or Drags file to App.
216 	if (!IsLaunching())
217 		return;
218 
219 	entry_ref ref;
220 	if (message->FindRef("refs", 0, &ref) != B_OK)
221 		return;
222 
223 	BFile file;
224 	if (file.SetTo(&ref, B_READ_WRITE) != B_OK)
225 		return;
226 
227 	BNodeInfo info(&file);
228 	char mimetype[B_MIME_TYPE_LENGTH];
229 	info.GetType(mimetype);
230 
231 	// if App opened by Pref file
232 	if (!strcmp(mimetype, PREFFILE_MIMETYPE)) {
233 
234 		BEntry ent(&ref);
235 		BPath path(&ent);
236 		PrefHandler::Default()->OpenText(path.Path());
237 		return;
238 	}
239 
240 	// if App opened by Shell Script
241 	if (!strcmp(mimetype, "text/x-haiku-shscript")){
242 		// Not implemented.
243 		//    beep();
244 		return;
245 	}
246 }
247 
248 
249 status_t
250 TermApp::_GetWindowPositionFile(BFile* file, uint32 openMode)
251 {
252 	BPath path;
253 	status_t status = find_directory(B_USER_SETTINGS_DIRECTORY, &path, true);
254 	if (status != B_OK)
255 		return status;
256 
257 	status = path.Append("Terminal_windows");
258 	if (status != B_OK)
259 		return status;
260 
261 	return file->SetTo(path.Path(), openMode);
262 }
263 
264 
265 status_t
266 TermApp::_LoadWindowPosition(BRect* frame, uint32* workspaces)
267 {
268 	status_t status = B_ERROR;
269 	BMessage position = BMessage();
270 
271 	BFile file;
272 	status = _GetWindowPositionFile(&file, B_READ_ONLY);
273 	if (status != B_OK)
274 		return status;
275 
276 	status = position.Unflatten(&file);
277 
278 	file.Unset();
279 
280 	if (status != B_OK)
281 		return status;
282 
283 	status = position.FindRect("rect", fWindowNumber - 1, frame);
284 	if (status != B_OK)
285 		return status;
286 
287 	int32 _workspaces;
288 	status = position.FindInt32("workspaces", fWindowNumber - 1, &_workspaces);
289 	if (status != B_OK)
290 		return status;
291 	if (modifiers() & B_OPTION_KEY)
292 		*workspaces = _workspaces;
293 
294 	printf("loading settings ok\n");
295 	return B_OK;
296 }
297 
298 
299 status_t
300 TermApp::_SaveWindowPosition(BMessage* position)
301 {
302 	BFile file;
303 	BMessage originalSettings;
304 
305 	// We append ourself to the existing settings file
306 	// So we have to read it, insert our BMessage, and rewrite it.
307 
308 	status_t status = _GetWindowPositionFile(&file, B_READ_ONLY);
309 	if (status == B_OK) {
310 		originalSettings.Unflatten(&file);
311 			// No error checking on that : it fails if the settings
312 			// file is missing, but we can create it.
313 
314 		file.Unset();
315 	}
316 
317 	// Append the new settings
318 	BRect rect;
319 	position->FindRect("rect", &rect);
320 	if (originalSettings.ReplaceRect("rect", fWindowNumber - 1, rect) != B_OK)
321 		originalSettings.AddRect("rect", rect);
322 
323 	int32 workspaces;
324 	position->FindInt32("workspaces", &workspaces);
325 	if (originalSettings.ReplaceInt32("workspaces", fWindowNumber - 1, workspaces)
326 			!= B_OK)
327 		originalSettings.AddInt32("workspaces", workspaces);
328 
329 	// Resave the whole thing
330 	status = _GetWindowPositionFile (&file,
331 		B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE);
332 	if (status != B_OK)
333 		return status;
334 
335 	return originalSettings.Flatten(&file);
336 }
337 
338 
339 status_t
340 TermApp::_MakeTermWindow(BRect &frame, uint32 workspaces)
341 {
342 	try {
343 		fTermWindow = new TermWindow(frame, fWindowTitle.String(), workspaces,
344 			fArgs);
345 	} catch (int error) {
346 		return (status_t)error;
347 	} catch (...) {
348 		return B_ERROR;
349 	}
350 
351 	fTermWindow->Show();
352 
353 	return B_OK;
354 }
355 
356 
357 void
358 TermApp::_ActivateTermWindow(team_id id)
359 {
360 	BMessenger app(TERM_SIGNATURE, id);
361 	if (app.IsTargetLocal())
362 		fTermWindow->Activate();
363 	else
364 		app.SendMessage(MSG_ACTIVATE_TERM);
365 }
366 
367 
368 void
369 TermApp::_SwitchTerm()
370 {
371 	team_id myId = be_app->Team();
372 	BList teams;
373 	be_roster->GetAppList(TERM_SIGNATURE, &teams);
374 
375 	int32 numTerms = teams.CountItems();
376 	if (numTerms <= 1)
377 		return;
378 
379 	// Find our position in the app teams.
380 	int32 i;
381 
382 	for (i = 0; i < numTerms; i++) {
383 		if (myId == reinterpret_cast<team_id>(teams.ItemAt(i)))
384 			break;
385 	}
386 
387 	do {
388 		if (--i < 0)
389 			i = numTerms - 1;
390 	} while (!_IsSwitchTarget(reinterpret_cast<team_id>(teams.ItemAt(i))));
391 
392 	// Activate switched terminal.
393 	_ActivateTermWindow(reinterpret_cast<team_id>(teams.ItemAt(i)));
394 }
395 
396 
397 bool
398 TermApp::_IsSwitchTarget(team_id id)
399 {
400 	uint32 currentWorkspace = 1L << current_workspace();
401 
402 	BMessenger app(TERM_SIGNATURE, id);
403 	if (app.IsTargetLocal()) {
404 		return !fTermWindow->IsMinimized()
405 			&& (fTermWindow->Workspaces() & currentWorkspace) != 0;
406 	}
407 
408 	BMessage reply;
409 	if (app.SendMessage(MSG_TERM_WINDOW_INFO, &reply) != B_OK)
410 		return false;
411 
412 	bool minimized;
413 	int32 workspaces;
414 	if (reply.FindBool("minimized", &minimized) != B_OK
415 		|| reply.FindInt32("workspaces", &workspaces) != B_OK)
416 		return false;
417 
418 	return !minimized && (workspaces & currentWorkspace) != 0;
419 }
420 
421 
422 /*!	Checks if all teams that have an ID-to-team mapping in the message
423 	are still running.
424 	The IDs for teams that are gone will be made available again, and
425 	their mapping is removed from the message.
426 */
427 void
428 TermApp::_SanitizeIDs(BMessage* data, uint8* windows, ssize_t length)
429 {
430 	BList teams;
431 	be_roster->GetAppList(TERM_SIGNATURE, &teams);
432 
433 	for (int32 i = 0; i < length; i++) {
434 		if (!windows[i])
435 			continue;
436 
437 		BString id("id-");
438 		id << i + 1;
439 
440 		team_id team;
441 		if (data->FindInt32(id.String(), &team) != B_OK)
442 			continue;
443 
444 		if (!teams.HasItem((void*)team)) {
445 			windows[i] = false;
446 			data->RemoveName(id.String());
447 		}
448 	}
449 }
450 
451 
452 /*!
453 	Removes the current fWindowNumber (ID) from the supplied array, or
454 	finds a free ID in it, and sets fWindowNumber accordingly.
455 */
456 bool
457 TermApp::_UpdateIDs(bool set, uint8* windows, ssize_t maxLength,
458 	ssize_t* _length)
459 {
460 	ssize_t length = *_length;
461 
462 	if (set) {
463 		int32 i;
464 		for (i = 0; i < length; i++) {
465 			if (!windows[i]) {
466 				windows[i] = true;
467 				fWindowNumber = i + 1;
468 				break;
469 			}
470 		}
471 
472 		if (i == length) {
473 			if (length >= maxLength)
474 				return false;
475 
476 			windows[length] = true;
477 			length++;
478 			fWindowNumber = length;
479 		}
480 	} else {
481 		// update information and write it back
482 		windows[fWindowNumber - 1] = false;
483 	}
484 
485 	*_length = length;
486 	return true;
487 }
488 
489 
490 void
491 TermApp::_UpdateRegistration(bool set)
492 {
493 	if (set)
494 		fWindowNumber = -1;
495 	else if (fWindowNumber < 0)
496 		return;
497 
498 #ifdef __HAIKU__
499 	// use BClipboard - it supports atomic access in Haiku
500 	BClipboard clipboard(TERM_SIGNATURE);
501 
502 	while (true) {
503 		if (!clipboard.Lock())
504 			return;
505 
506 		BMessage* data = clipboard.Data();
507 
508 		const uint8* windowsData;
509 		uint8 windows[512];
510 		ssize_t length;
511 		if (data->FindData("ids", B_RAW_TYPE,
512 				(const void**)&windowsData, &length) != B_OK)
513 			length = 0;
514 
515 		if (length > (ssize_t)sizeof(windows))
516 			length = sizeof(windows);
517 		if (length > 0)
518 			memcpy(windows, windowsData, length);
519 
520 		_SanitizeIDs(data, windows, length);
521 
522 		status_t status = B_OK;
523 		if (_UpdateIDs(set, windows, sizeof(windows), &length)) {
524 			// add/remove our ID-to-team mapping
525 			BString id("id-");
526 			id << fWindowNumber;
527 
528 			if (set)
529 				data->AddInt32(id.String(), Team());
530 			else
531 				data->RemoveName(id.String());
532 
533 			data->RemoveName("ids");
534 			//if (data->ReplaceData("ids", B_RAW_TYPE, windows, length) != B_OK)
535 			data->AddData("ids", B_RAW_TYPE, windows, length);
536 
537 			status = clipboard.Commit(true);
538 		}
539 
540 		clipboard.Unlock();
541 
542 		if (status == B_OK)
543 			break;
544 	}
545 #else	// !__HAIKU__
546 	// use a file to store the IDs - unfortunately, locking
547 	// doesn't work on BeOS either here
548 	int fd = open("/tmp/terminal_ids", O_RDWR | O_CREAT);
549 	if (fd < 0)
550 		return;
551 
552 	struct flock lock;
553 	lock.l_type = F_WRLCK;
554 	lock.l_whence = SEEK_CUR;
555 	lock.l_start = 0;
556 	lock.l_len = -1;
557 	fcntl(fd, F_SETLKW, &lock);
558 
559 	uint8 windows[512];
560 	ssize_t length = read_pos(fd, 0, windows, sizeof(windows));
561 	if (length < 0) {
562 		close(fd);
563 		return;
564 	}
565 
566 	if (length > (ssize_t)sizeof(windows))
567 		length = sizeof(windows);
568 
569 	if (_UpdateIDs(set, windows, sizeof(windows), &length))
570 		write_pos(fd, 0, windows, length);
571 
572 	close(fd);
573 #endif	// !__HAIKU__
574 }
575 
576 
577 void
578 TermApp::_UnregisterTerminal()
579 {
580 	_UpdateRegistration(false);
581 }
582 
583 
584 void
585 TermApp::_RegisterTerminal()
586 {
587 	_UpdateRegistration(true);
588 }
589 
590 
591 //#ifndef B_NETPOSITIVE_APP_SIGNATURE
592 //#define B_NETPOSITIVE_APP_SIGNATURE "application/x-vnd.Be-NPOS"
593 //#endif
594 //
595 //void
596 //TermApp::ShowHTML(BMessage *msg)
597 //{
598 //  const char *url;
599 //  msg->FindString("Url", &url);
600 //  BMessage message;
601 //
602 //  message.what = B_NETPOSITIVE_OPEN_URL;
603 //  message.AddString("be:url", url);
604 
605 //  be_roster->Launch(B_NETPOSITIVE_APP_SIGNATURE, &message);
606 //  while(!(be_roster->IsRunning(B_NETPOSITIVE_APP_SIGNATURE)))
607 //    snooze(10000);
608 //
609 //  // Activate net+
610 //  be_roster->ActivateApp(be_roster->TeamFor(B_NETPOSITIVE_APP_SIGNATURE));
611 //}
612 
613 
614 void
615 TermApp::_HandleChildCleanup()
616 {
617 }
618 
619 
620 /*static*/ void
621 TermApp::_SigChildHandler(int signal, void* data)
622 {
623 	// Spawing a thread that does the actual signal handling is pretty much
624 	// the only safe thing to do in a multi-threaded application. The
625 	// interrupted thread might have been anywhere, e.g. in a critical section,
626 	// holding locks. If we do anything that does require locking at any point
627 	// (e.g. memory allocation, messaging), we risk a dead-lock or data
628 	// structure corruption. Spawing a thread is safe though, since its only
629 	// a system call.
630 	thread_id thread = spawn_thread(_ChildCleanupThread, "child cleanup",
631 		B_NORMAL_PRIORITY, ((TermApp*)data)->fTermWindow);
632 	if (thread >= 0)
633 		resume_thread(thread);
634 }
635 
636 
637 /*static*/ status_t
638 TermApp::_ChildCleanupThread(void* data)
639 {
640 	// Just drop the windowa message and let it do the actual work. This
641 	// saves us additional synchronization measures.
642 	return ((TermWindow*)data)->PostMessage(MSG_CHECK_CHILDREN);
643 }
644 
645 
646 
647 void
648 TermApp::_Usage(char *name)
649 {
650 	fprintf(stderr, B_TRANSLATE("Haiku Terminal\n"
651 		"Copyright 2001-2009 Haiku, Inc.\n"
652 		"Copyright(C) 1999 Kazuho Okui and Takashi Murai.\n"
653 		"\n"
654 		"Usage: %s [OPTION] [SHELL]\n"), name);
655 
656 	fprintf(stderr,
657 		B_TRANSLATE("  -h,     --help               print this help\n"
658 		//"  -p,     --preference         load preference file\n"
659 		"  -t,     --title              set window title\n"
660 		"  -f,     --fullscreen         start fullscreen\n")
661 		//"  -geom,  --geometry           set window geometry\n"
662 		//"                               An example of geometry is \"80x25+100+100\"\n"
663 		);
664 }
665 
666