MODULE WMShell; (** AUTHOR "staubesv"; PURPOSE "GUI for shell"; *)
(**
 * Usage:
 *
 *	WMShell.Open ~ opens new shell
 *	SystemTools.Free WMShell ~ closes all open shells
 *
 * History:
 *
 *	25.06.2007	First release (staubesv)
 *
 * TODO:
 * 	- nice shutdown when freeing module
 *)

IMPORT
	Shell, Streams, Pipes, Texts, TextUtilities, Strings,
	Modules, Kernel, Inputs,
	WMGraphics, WMWindowManager, WMMessages, WMRestorable,
	WMComponents, WMDocumentEditor;

CONST

	(* Default size of window at start up *)
	DefaultWidth = 640; DefaultHeight = 300;

	ReceiveBufferSize = 256;

	Prompt = "SHELL>";

	Backspace = 08X;
	ESC = 1BX;
	DEL = 7FX;

TYPE

	ShellComponent = OBJECT(WMComponents.VisualComponent)
	VAR
		out : Streams.Writer;
		in : Streams.Reader;

		pipeOut, pipeIn : Pipes.Pipe;

		(* Terminal window text writer *)
		w : TextUtilities.TextWriter;
		text : Texts.Text;

		shell : Shell.Shell;

		editor : WMDocumentEditor.Editor;

		running, dead : BOOLEAN;
		timer : Kernel.Timer;

		PROCEDURE Clear;
		BEGIN
			editor.Clear;
			Invalidate;
		END Clear;

		PROCEDURE ExtPointerUp(x, y : LONGINT; keys : SET; VAR handled : BOOLEAN);
		BEGIN
			text.AcquireRead;
			editor.editor.tv.cursor.SetPosition(text.GetLength());
			text.ReleaseRead;
		END ExtPointerUp;

		PROCEDURE ExtKeyPressed(ucs : LONGINT; flags : SET; VAR keySym : LONGINT; VAR handled : BOOLEAN);
		BEGIN
			handled := FALSE;
			IF editor.HandleShortcut(ucs, flags, keySym) THEN handled := TRUE; END;

			IF ~handled & ~(Inputs.Release IN flags) THEN
				handled := TRUE;
				IF keySym = 01H THEN (* Ctrl-A *)
					editor.editor.tv.SelectAll
				ELSIF keySym = 03H THEN (* Ctrl-C *)
					editor.editor.tv.CopySelection
				ELSIF keySym = 16H THEN (* Ctrl-V *)
					CopyFromClipboard;
	 			ELSIF (keySym = 0FF63H) & (flags * Inputs.Ctrl # {}) THEN  (*Ctrl Insert *)
	 				editor.editor.tv.CopySelection
				ELSIF keySym = 0FF56H THEN (* Page Down *)
					editor.editor.tv.PageDown(flags * Inputs.Shift # {})
				ELSIF keySym = 0FF55H THEN (* Page Up *)
					editor.editor.tv.PageUp(flags * Inputs.Shift # {})
				ELSIF keySym = 0FF50H THEN (* Cursor Home *)
					editor.editor.tv.Home(flags * Inputs.Ctrl # {}, flags * Inputs.Shift # {})
				ELSIF keySym = 0FF57H THEN (* Cursor End *)
					editor.editor.tv.End(flags * Inputs.Ctrl # {}, flags * Inputs.Shift # {})
				ELSIF keySym = 0FF1BH THEN (* Esc *)
					IF ~(Inputs.Release IN flags) THEN
						out.Char(ESC); out.Char(ESC); out.Update;
					END;
				ELSE
					handled := FALSE;
				END
			END;

			IF ~handled & (ucs > 0) & (ucs < 256) THEN
				IF ~(Inputs.Release IN flags) THEN
					IF ucs > 127 THEN (* Escape non-ascii characters *)
						out.Char(ESC); out.Char("["); ucs := ucs - 128;
					END;
					out.Char(CHR(ucs)); out.Update;
				END;
				handled := TRUE;
			END;
		END ExtKeyPressed;

		PROCEDURE Wait(ms : LONGINT);
		BEGIN
			timer.Sleep(ms);
		END Wait;

		PROCEDURE InitShell;
		VAR shellIn : Streams.Reader; shellOut : Streams.Writer;
		BEGIN
			NEW(pipeOut, 256); NEW(pipeIn, 256);

			(* wire pipes *)
			NEW(shellIn, pipeOut.Receive, 256);
			NEW(shellOut, pipeIn.Send, 256);

			NEW(out, pipeOut.Send, 256);
			NEW(in, pipeIn.Receive, 256);

			NEW(shell, shellIn,shellOut, shellOut, TRUE, Prompt);
		END InitShell;

		PROCEDURE CopyFromClipboard;
		VAR string : POINTER TO ARRAY OF CHAR;
		BEGIN
			Texts.clipboard.AcquireRead;
			IF Texts.clipboard.GetLength() > 0 THEN
				NEW(string, Texts.clipboard.GetLength()+1);
				TextUtilities.TextToStr(Texts.clipboard, string^);
			END;
			Texts.clipboard.ReleaseRead;
			out.String(string^); out.Update;
		END CopyFromClipboard;

		PROCEDURE Finalize;
		BEGIN
(*			pipeIn.Close; pipeOut.Close; *)
			shell.Exit;
			BEGIN {EXCLUSIVE}
				running := FALSE;
				AWAIT(dead);
			END;
		END Finalize;

		PROCEDURE DeleteNCharacters(nbrOfCharacters : LONGINT);
		VAR pos : LONGINT;
		BEGIN
			text.AcquireWrite;
			pos := editor.editor.tv.cursor.GetPosition();
			text.Delete(pos - nbrOfCharacters, nbrOfCharacters);
			text.ReleaseWrite;
		END DeleteNCharacters;

		PROCEDURE ReceiveCharacters;
		VAR ch : CHAR; buffer : ARRAY ReceiveBufferSize OF CHAR; backspaces, i, size, len : LONGINT;
		BEGIN
			(* Receive at least one character *)
			size := in.Available();
			IF size > ReceiveBufferSize THEN size := ReceiveBufferSize; END;
			in.Bytes(buffer, 0, size, len);
			IF in.res = Streams.Ok THEN
				FOR i := 0 TO len-1 DO
					ch := buffer[i];
					IF (ch = DEL) OR (ch = Backspace) THEN
						INC(backspaces);
					ELSE
						IF (backspaces > 0) THEN
							w.Update;
							DeleteNCharacters(backspaces);
							backspaces := 0;
						END;
						w.Char(ch);
					END;
				END;
				w.Update;
			END;
			DeleteNCharacters(backspaces);
		END ReceiveCharacters;

		PROCEDURE &Init*;
		BEGIN
			Init^;
			running := TRUE;
			NEW(timer);

			NEW(editor); editor.alignment.Set(WMComponents.AlignClient);
			editor.SetToolbar(WMDocumentEditor.StoreButton + WMDocumentEditor.WrapButton + WMDocumentEditor.SearchButton + WMDocumentEditor.ClearButton);
			editor.editor.tv.SetExtKeyEventHandler(ExtKeyPressed);
			editor.editor.tv.SetExtPointerUpHandler(ExtPointerUp);
			AddContent(editor);

			NEW(text);
			NEW(w, text); w.SetFontName("Courier");
			editor.SetText(text);
			InitShell;
			SetNameAsString(StrShellComponent);
		END Init;

	BEGIN {ACTIVE}
		WHILE running DO
			IF running & (in.Available() > 0) THEN ReceiveCharacters; END;
			Wait(2);
		END;
		BEGIN {EXCLUSIVE} dead := TRUE; END;
	END ShellComponent;

TYPE

	KillerMsg = OBJECT
	END KillerMsg;

	Window* = OBJECT (WMComponents.FormWindow)
	VAR
		shell : ShellComponent;

		PROCEDURE HandleUpcall(command : LONGINT);
		BEGIN
			IF command = Shell.Clear THEN
				shell.Clear;
			ELSIF command = Shell.ExitShell THEN
				Close;
			END;
		END HandleUpcall;

		PROCEDURE &New*(c : WMRestorable.Context);
		BEGIN
			IncCount;

			NEW(shell); shell.alignment.Set(WMComponents.AlignClient);
			shell.shell.SetUpcall(HandleUpcall);

			Init(DefaultWidth, DefaultHeight, FALSE);

			SetContent(shell);
			SetTitle(Strings.NewString("BlueShell"));
			SetIcon(WMGraphics.LoadImage("WMIcons.tar://WMShell.png", TRUE));

			IF c # NIL THEN
				WMRestorable.AddByContext(SELF, c);
				Resized(GetWidth(), GetHeight());
			ELSE
				WMWindowManager.DefaultAddWindow(SELF);
			END;
			shell.editor.editor.SetFocus();
		END New;

		PROCEDURE Close;
		BEGIN
			Close^;
			DecCount
		END Close;

		PROCEDURE Handle(VAR x : WMMessages.Message);
		BEGIN
			IF (x.msgType = WMMessages.MsgExt) & (x.ext # NIL) THEN
				IF (x.ext IS KillerMsg) THEN
					Close;
				ELSIF (x.ext IS WMRestorable.Storage) THEN
					x.ext(WMRestorable.Storage).Add("Shell", "WMShell.Restore", SELF, NIL);
				ELSE
					Handle^(x);
				END;
			ELSE Handle^(x)
			END
		END Handle;

	END Window;

VAR
	nofWindows : LONGINT;

	StrShellComponent : Strings.String;

PROCEDURE InitStrings;
BEGIN
	StrShellComponent := Strings.NewString("ShellComponent");
END InitStrings;

PROCEDURE Restore*(context : WMRestorable.Context);
VAR window : Window;
BEGIN
	ASSERT(context # NIL);
	NEW(window, context);
END Restore;

PROCEDURE Open*;
VAR window : Window;
BEGIN
	NEW(window, NIL);
END Open;

PROCEDURE IncCount;
BEGIN {EXCLUSIVE}
	INC(nofWindows)
END IncCount;

PROCEDURE DecCount;
BEGIN {EXCLUSIVE}
	DEC(nofWindows)
END DecCount;

PROCEDURE Cleanup;
VAR die : KillerMsg;
	 msg : WMMessages.Message;
	 m : WMWindowManager.WindowManager;
BEGIN {EXCLUSIVE}
	NEW(die);
	msg.ext := die;
	msg.msgType := WMMessages.MsgExt;
	m := WMWindowManager.GetDefaultManager();
	m.Broadcast(msg);
	AWAIT(nofWindows = 0)
END Cleanup;

BEGIN
	Modules.InstallTermHandler(Cleanup);
	InitStrings;
END WMShell.

WMShell.Open ~

SystemTools.Free WMShell ~
SystemTools.Free WMShell Shell ~