View Javadoc
1   package org.argeo.swt.desktop;
2   
3   import java.io.BufferedReader;
4   import java.io.IOException;
5   import java.io.InputStreamReader;
6   import java.io.OutputStream;
7   import java.net.InetAddress;
8   import java.net.UnknownHostException;
9   import java.nio.charset.Charset;
10  import java.nio.charset.StandardCharsets;
11  import java.nio.file.Files;
12  import java.nio.file.Path;
13  import java.nio.file.Paths;
14  import java.util.ArrayList;
15  import java.util.Arrays;
16  import java.util.List;
17  import java.util.StringTokenizer;
18  
19  import org.eclipse.swt.SWT;
20  import org.eclipse.swt.events.KeyEvent;
21  import org.eclipse.swt.events.KeyListener;
22  import org.eclipse.swt.events.PaintEvent;
23  import org.eclipse.swt.events.PaintListener;
24  import org.eclipse.swt.graphics.Font;
25  import org.eclipse.swt.graphics.GC;
26  import org.eclipse.swt.graphics.Point;
27  import org.eclipse.swt.layout.GridData;
28  import org.eclipse.swt.layout.GridLayout;
29  import org.eclipse.swt.widgets.Canvas;
30  import org.eclipse.swt.widgets.Caret;
31  import org.eclipse.swt.widgets.Composite;
32  import org.eclipse.swt.widgets.Display;
33  import org.eclipse.swt.widgets.Shell;
34  
35  public class MiniTerminal implements KeyListener, PaintListener {
36  
37  	private Canvas area;
38  	private Caret caret;
39  
40  	private StringBuffer buf = new StringBuffer("");
41  	private StringBuffer userInput = new StringBuffer("");
42  	private List<String> history = new ArrayList<>();
43  
44  	private Point charExtent = null;
45  	private int charsPerLine = 0;
46  	private String[] lines = new String[0];
47  	private List<String> logicalLines = new ArrayList<>();
48  
49  	private Font mono;
50  	private Charset charset;
51  
52  	private Path currentDir;
53  	private Path homeDir;
54  	private String host = "localhost";
55  	private String username;
56  
57  	// Sub process
58  	private Process process;
59  	private boolean running = false;
60  	private OutputStream stdIn = null;
61  
62  	private Thread readOut;
63  
64  	public MiniTerminal(Composite parent, int style) {
65  		charset = StandardCharsets.UTF_8;
66  
67  		Display display = parent.getDisplay();
68  		// Linux-specific
69  		mono = new Font(display, "Monospace", 10, SWT.NONE);
70  
71  		parent.setLayout(new GridLayout());
72  		area = new Canvas(parent, SWT.NONE);
73  		area.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
74  		caret = new Caret(area, SWT.NONE);
75  		area.setCaret(caret);
76  
77  		area.addKeyListener(this);
78  		area.addPaintListener(this);
79  
80  		username = System.getProperty("user.name");
81  		try {
82  			host = InetAddress.getLocalHost().getHostName();
83  			if (host.indexOf('.') > 0)
84  				host = host.substring(0, host.indexOf('.'));
85  		} catch (UnknownHostException e) {
86  			host = "localhost";
87  		}
88  		homeDir = Paths.get(System.getProperty("user.home"));
89  		currentDir = homeDir;
90  
91  		buf = new StringBuffer(prompt());
92  	}
93  
94  	@Override
95  	public void keyPressed(KeyEvent e) {
96  	}
97  
98  	@Override
99  	public void keyReleased(KeyEvent e) {
100 		if (e.keyLocation != 0)
101 			return;// weird characters
102 		// System.out.println(e.character);
103 		if (e.keyCode == 0xd) {// return
104 			markLogicalLine();
105 			if (!running)
106 				processUserInput();
107 			// buf.append(prompt());
108 		} else if (e.keyCode == 0x8) {// delete
109 			if (userInput.length() == 0)
110 				return;
111 			userInput.setLength(userInput.length() - 1);
112 			if (!running && buf.length() > 0)
113 				buf.setLength(buf.length() - 1);
114 		} else if (e.stateMask == 0x40000 && e.keyCode == 0x63) {// Ctrl+C
115 			if (process != null)
116 				process.destroy();
117 		} else if (e.stateMask == 0x40000 && e.keyCode == 0xdf) {// Ctrl+\
118 			if (process != null) {
119 				process.destroyForcibly();
120 			}
121 		} else {
122 			// if (!running)
123 			buf.append(e.character);
124 			userInput.append(e.character);
125 		}
126 
127 		if (area.isDisposed())
128 			return;
129 		area.redraw();
130 		// System.out.println("Append " + e);
131 
132 		if (running) {
133 			if (stdIn != null) {
134 				try {
135 					stdIn.write(Character.toString(e.character).getBytes(charset));
136 				} catch (IOException e1) {
137 					// TODO Auto-generated catch block
138 					e1.printStackTrace();
139 				}
140 			}
141 		}
142 	}
143 
144 	protected String prompt() {
145 		String fileName = currentDir.equals(homeDir) ? "~" : currentDir.getFileName().toString();
146 		String end = username.equals("root") ? "]# " : "]$ ";
147 		return "[" + username + "@" + host + " " + fileName + end;
148 	}
149 
150 	private void displayPrompt() {
151 		buf.append(prompt() + userInput);
152 	}
153 
154 	protected void markLogicalLine() {
155 		String str = buf.toString().trim();
156 		logicalLines.add(str);
157 		buf = new StringBuffer("");
158 	}
159 
160 	private void processUserInput() {
161 		String cmd = userInput.toString();
162 		userInput = new StringBuffer("");
163 		processUserInput(cmd);
164 		history.add(cmd);
165 	}
166 
167 	protected void processUserInput(String input) {
168 		try {
169 			StringTokenizer st = new StringTokenizer(input);
170 			List<String> args = new ArrayList<>();
171 			while (st.hasMoreTokens())
172 				args.add(st.nextToken());
173 			if (args.size() == 0) {
174 				displayPrompt();
175 				return;
176 			}
177 
178 			// change directory
179 			if (args.get(0).equals("cd")) {
180 				if (args.size() == 1) {
181 					setPath(homeDir);
182 				} else {
183 					Path newPath = currentDir.resolve(args.get(1));
184 					if (!Files.exists(newPath) || !Files.isDirectory(newPath)) {
185 						println(newPath + ": No such file or directory");
186 						return;
187 					}
188 					setPath(newPath);
189 				}
190 				displayPrompt();
191 				return;
192 			}
193 			// show current directory
194 			else if (args.get(0).equals("pwd")) {
195 				println(currentDir);
196 				displayPrompt();
197 				return;
198 			}
199 			// exit
200 			else if (args.get(0).equals("exit")) {
201 				println("logout");
202 				area.getShell().dispose();
203 				return;
204 			}
205 
206 			ProcessBuilder pb = new ProcessBuilder(args);
207 			pb.redirectErrorStream(true);
208 			pb.directory(currentDir.toFile());
209 //			Process process = Runtime.getRuntime().exec(input, null, currentPath.toFile());
210 			process = pb.start();
211 
212 			stdIn = process.getOutputStream();
213 			readOut = new Thread("MinitTerminal read out") {
214 				@Override
215 				public void run() {
216 					running = true;
217 					try (BufferedReader in = new BufferedReader(
218 							new InputStreamReader(process.getInputStream(), charset))) {
219 						String line = null;
220 						while ((line = in.readLine()) != null) {
221 							println(line);
222 						}
223 					} catch (IOException e) {
224 						println(e.getMessage());
225 					}
226 					stdIn = null;
227 					displayPrompt();
228 					running = false;
229 					readOut = null;
230 					process = null;
231 				}
232 			};
233 			readOut.start();
234 		} catch (IOException e) {
235 			println(e.getMessage());
236 			displayPrompt();
237 		}
238 	}
239 
240 	protected int linesForLogicalLine(char[] line) {
241 		return line.length / charsPerLine + 1;
242 	}
243 
244 	protected void println(Object line) {
245 		buf.append(line);
246 		markLogicalLine();
247 	}
248 
249 	protected void refreshLines(int charPerLine, int nbrOfLines) {
250 		if (lines.length != nbrOfLines) {
251 			lines = new String[nbrOfLines];
252 			Arrays.fill(lines, null);
253 		}
254 		if (this.charsPerLine != charPerLine)
255 			this.charsPerLine = charPerLine;
256 
257 		int currentLine = nbrOfLines - 1;
258 		// current line
259 		if (buf.length() > 0) {
260 			lines[currentLine] = buf.toString();
261 		} else {
262 			lines[currentLine] = "";
263 		}
264 		currentLine--;
265 
266 		logicalLines: for (int i = logicalLines.size() - 1; i >= 0; i--) {
267 			char[] logicalLine = logicalLines.get(i).toCharArray();
268 			int linesNeeded = linesForLogicalLine(logicalLine);
269 			for (int j = linesNeeded - 1; j >= 0; j--) {
270 				int from = j * charPerLine;
271 				int to = j == linesNeeded - 1 ? from + charPerLine : Math.min(from + charPerLine, logicalLine.length);
272 				lines[currentLine] = new String(Arrays.copyOfRange(logicalLine, from, to));
273 //				System.out.println("Set line " + currentLine + " to : " + lines[currentLine]);
274 				currentLine--;
275 				if (currentLine < 0)
276 					break logicalLines;
277 			}
278 		}
279 	}
280 
281 	@Override
282 	public void paintControl(PaintEvent e) {
283 		GC gc = e.gc;
284 		gc.setFont(mono);
285 		if (charExtent == null)
286 			charExtent = gc.textExtent("a");
287 
288 		Point areaSize = area.getSize();
289 		int charPerLine = areaSize.x / charExtent.x;
290 		int nbrOfLines = areaSize.y / charExtent.y;
291 		refreshLines(charPerLine, nbrOfLines);
292 
293 		for (int i = 0; i < lines.length; i++) {
294 			String line = lines[i];
295 			if (line != null)
296 				gc.drawString(line, 0, i * charExtent.y);
297 		}
298 //		String toDraw = buf.toString();
299 //		gc.drawString(toDraw, 0, 0);
300 //		area.setCaret(caret);
301 	}
302 
303 	public void setPath(String path) {
304 		this.currentDir = Paths.get(path);
305 	}
306 
307 	public void setPath(Path path) {
308 		this.currentDir = path;
309 	}
310 
311 	public static void main(String[] args) {
312 		Display display = Display.getCurrent() == null ? new Display() : Display.getCurrent();
313 		Shell shell = new Shell(display, SWT.SHELL_TRIM);
314 
315 		MiniTerminal miniBrowser = new MiniTerminal(shell, SWT.NONE);
316 		String url = args.length > 0 ? args[0] : System.getProperty("user.home");
317 		miniBrowser.setPath(url);
318 
319 		shell.open();
320 		shell.setSize(new Point(800, 480));
321 		while (!shell.isDisposed()) {
322 			if (!display.readAndDispatch())
323 				display.sleep();
324 		}
325 	}
326 
327 }