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