/*
* XTermWidget.java
*
* Copyright (C) 2009-17 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.studio.client.workbench.views.terminal.xterm;
import org.rstudio.core.client.CommandWithArg;
import org.rstudio.core.client.ExternalJavaScriptLoader;
import org.rstudio.core.client.ExternalJavaScriptLoader.Callback;
import org.rstudio.core.client.StringSink;
import org.rstudio.core.client.resources.StaticDataResource;
import org.rstudio.core.client.theme.res.ThemeStyles;
import org.rstudio.core.client.widget.FontSizer;
import org.rstudio.studio.client.common.SuperDevMode;
import org.rstudio.studio.client.workbench.views.console.ConsoleResources;
import org.rstudio.studio.client.workbench.views.terminal.events.ResizeTerminalEvent;
import org.rstudio.studio.client.workbench.views.terminal.events.TerminalDataInputEvent;
import org.rstudio.studio.client.workbench.views.terminal.events.XTermTitleEvent;
import org.rstudio.studio.client.workbench.views.terminal.events.XTermTitleEvent.Handler;
import org.rstudio.studio.client.workbench.views.terminal.xterm.XTermDimensions;
import org.rstudio.studio.client.workbench.views.terminal.xterm.XTermNative;
import org.rstudio.studio.client.workbench.views.terminal.xterm.XTermResources;
import org.rstudio.studio.client.workbench.views.terminal.xterm.XTermThemeResources;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.Document;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.LinkElement;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.ui.RequiresResize;
import com.google.gwt.user.client.ui.Widget;
/**
* Xterm-compatible terminal emulator widget. This widget does no network
* communication.
*
* To receive input (user typing), subscribe to TerminalDataInputEvent, or
* use addDataEventHandler, which stops TerminalDataInputEvent from being
* fired and makes a direct callback.
*
* To send output to the terminal, use write() or writeln().
*
* To receive notice of terminal resizes, subscribe to ResizeTerminalEvent.
*
* For title changes (via escape sequences sent to terminal), subscribe to
* XTermTitleEvent.
*/
public class XTermWidget extends Widget implements RequiresResize,
ResizeTerminalEvent.HasHandlers,
TerminalDataInputEvent.HasHandlers,
XTermTitleEvent.HasHandlers,
StringSink
{
/**
* Creates an XTermWidget.
*/
public XTermWidget(boolean cursorBlink, boolean focus)
{
// Create an element to hold the terminal widget
setElement(Document.get().createDivElement());
setStyleName(ConsoleResources.INSTANCE.consoleStyles().console());
getElement().setTabIndex(0);
getElement().getStyle().setMargin(0, Unit.PX);
getElement().addClassName(ThemeStyles.INSTANCE.selectableText());
getElement().addClassName(XTERM_CLASS);
getElement().addClassName("ace_editor");
// Create and attach the native terminal object to this Widget
attachTheme(XTermThemeResources.INSTANCE.xtermcss());
terminal_ = XTermNative.createTerminal(getElement(), cursorBlink, focus);
terminal_.addClass("ace_editor");
terminal_.addClass(FontSizer.getNormalFontSizeClass());
// Handle keystrokes from the xterm and dispatch them
addDataEventHandler(new CommandWithArg<String>()
{
public void execute(String data)
{
fireEvent(new TerminalDataInputEvent(data));
}
});
// Handle title events from the xterm and dispatch them
addTitleEventHandler(new CommandWithArg<String>()
{
public void execute(String title)
{
fireEvent(new XTermTitleEvent(title));
}
});
}
/**
* Perform actions when the terminal is ready.
*/
protected void terminalReady()
{
}
/**
* One one line of text to the terminal.
* @param str Text to write (CRLF will be appended)
*/
public void writeln(String str)
{
terminal_.scrollToBottom();
terminal_.writeln(str);
}
/**
* Write text to the terminal.
* @param str Text to write
*/
@Override
public void write(String str)
{
terminal_.scrollToBottom();
terminal_.write(str);
}
/**
* Clear terminal buffer.
*/
public void clear()
{
terminal_.clear();
setFocus(true);
}
/**
* Inject the xterm.js styles into the page.
* @param cssResource
*/
private void attachTheme(StaticDataResource cssResource)
{
if (currentStyleEl_ != null)
currentStyleEl_.removeFromParent();
currentStyleEl_ = Document.get().createLinkElement();
currentStyleEl_.setType("text/css");
currentStyleEl_.setRel("stylesheet");
currentStyleEl_.setHref(cssResource.getSafeUri().asString());
Document.get().getBody().appendChild(currentStyleEl_);
}
@Override
protected void onLoad()
{
super.onLoad();
if (!initialized_)
{
initialized_ = true;
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
@Override
public void execute()
{
terminal_.fit();
terminal_.focus();
terminalReady();
}
});
}
}
@Override
protected void onUnload()
{
super.onUnload();
if (initialized_)
{
initialized_ = false;
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
public void execute()
{
terminal_.blur();
}
});
}
}
@Override
public void onResize()
{
if (!isVisible())
{
return;
}
// Notify the local terminal UI that it has resized so it computes new
// dimensions; debounce this slightly as it is somewhat expensive
resizeTerminalLocal_.schedule(50);
// Notify the remote pseudo-terminal that it has resized; this is quite
// expensive so debounce more heavily; e.g. dragging the pane
// splitters or resizing the entire window
resizeTerminalRemote_.schedule(150);
}
private Timer resizeTerminalLocal_ = new Timer()
{
@Override
public void run()
{
terminal_.fit();
}
};
private Timer resizeTerminalRemote_ = new Timer()
{
@Override
public void run()
{
XTermDimensions size = getTerminalSize();
int cols = size.getCols();
int rows = size.getRows();
// ignore if a reasonable size couldn't be computed
if (cols < 1 || rows < 1)
{
return;
}
// don't send same size multiple times
if (cols == previousCols_ && rows == previousRows_)
{
return;
}
previousCols_ = cols;
previousRows_ = rows;
fireEvent(new ResizeTerminalEvent(cols, rows));
}
};
private void addDataEventHandler(CommandWithArg<String> handler)
{
terminal_.onTerminalData(handler);
}
private void addTitleEventHandler(CommandWithArg<String> handler)
{
terminal_.onTitleData(handler);
}
public XTermDimensions getTerminalSize()
{
return terminal_.proposeGeometry();
}
public void setFocus(boolean focused)
{
if (focused)
terminal_.focus();
else
terminal_.blur();
}
/**
* @return Current cursor column
*/
public int getCursorX()
{
return terminal_.cursorX();
}
/**
* @return Current cursor row
*/
public int getCursorY()
{
return terminal_.cursorY();
}
/**
* @return true if cursor at end of current line, false if not at EOL or
* terminal is showing alternate buffer
*/
public boolean cursorAtEOL()
{
if (altBufferActive())
{
return false;
}
String line = currentLine();
if (line == null)
{
return false;
}
for (int i = getCursorX(); i < line.length(); i++)
{
if (line.charAt(i) != ' ')
{
return false;
}
}
return true;
}
/**
* @return Text of current line buffer
*/
public String currentLine()
{
return terminal_.currentLine();
}
/**
* Is the terminal showing the alternate full-screen buffer?
* @return true if full-screen buffer is active
*/
public boolean altBufferActive()
{
return terminal_.altBufferActive();
}
public static boolean isXTerm(Element el)
{
while (el != null)
{
if (el.hasClassName(XTERM_CLASS))
return true;
el = el.getParentElement();
}
return false;
}
private static final ExternalJavaScriptLoader getLoader(StaticDataResource release,
StaticDataResource debug)
{
if (debug == null || !SuperDevMode.isActive())
return new ExternalJavaScriptLoader(release.getSafeUri().asString());
else
return new ExternalJavaScriptLoader(debug.getSafeUri().asString());
}
@Override
public HandlerRegistration addResizeTerminalHandler(ResizeTerminalEvent.Handler handler)
{
return addHandler(handler, ResizeTerminalEvent.TYPE);
}
@Override
public HandlerRegistration addTerminalDataInputHandler(TerminalDataInputEvent.Handler handler)
{
return addHandler(handler, TerminalDataInputEvent.TYPE);
}
@Override
public HandlerRegistration addXTermTitleHandler(Handler handler)
{
return addHandler(handler, XTermTitleEvent.TYPE);
}
/**
* Load resources for XTermWidget.
*
* @param command Command to execute after resources are loaded
*/
public static void load(final Command command)
{
xtermLoader_.addCallback(new Callback()
{
@Override
public void onLoaded()
{
xtermFitLoader_.addCallback(new Callback()
{
@Override
public void onLoaded()
{
if (command != null)
command.execute();
}
});
}
});
}
private static final ExternalJavaScriptLoader xtermLoader_ =
getLoader(XTermResources.INSTANCE.xtermjs(),
XTermResources.INSTANCE.xtermjsUncompressed());
private static final ExternalJavaScriptLoader xtermFitLoader_ =
getLoader(XTermResources.INSTANCE.xtermfitjs(),
XTermResources.INSTANCE.xtermfitjsUncompressed());
private XTermNative terminal_;
private LinkElement currentStyleEl_;
private boolean initialized_ = false;
private int previousRows_ = -1;
private int previousCols_ = -1;
private final static String XTERM_CLASS = "xterm-rstudio";
}