1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
9 var MOW = Module("mow", {
10 init: function init() {
12 this._resize = Timer(20, 400, function _resize() {
16 if (this.visible && isinstance(modes.main, modes.OUTPUT_MULTILINE))
17 this.updateMorePrompt();
20 this._timer = Timer(20, 400, function _timer() {
21 if (modes.have(modes.OUTPUT_MULTILINE)) {
24 if (options["more"] && this.canScroll(1))
25 // start the last executed command's output at the top of the screen
26 DOM(this.document.body.lastElementChild).scrollIntoView(true);
28 this.body.scrollTop = this.body.scrollHeight;
30 dactyl.focus(this.window);
31 this.updateMorePrompt();
35 events.listen(window, this, "windowEvents");
38 let fontSize = DOM(document.documentElement).style.fontSize;
39 styles.system.add("font-size", "dactyl://content/buffer.xhtml",
40 "body { font-size: " + fontSize + "; } \
41 html|html > xul|scrollbar { visibility: collapse !important; }",
44 XML.ignoreWhitespace = true;
45 overlay.overlayWindow(window, {
49 append: <e4x xmlns={XUL} xmlns:dactyl={NS}>
50 <window id={document.documentElement.id}>
52 <menupopup id="dactyl-contextmenu" highlight="Events" events="contextEvents">
53 <menuitem id="dactyl-context-copylink"
54 label={_("mow.contextMenu.copyLink")} dactyl:group="link"
55 oncommand="goDoCommand('cmd_copyLink');"/>
56 <menuitem id="dactyl-context-copypath"
57 label={_("mow.contextMenu.copyPath")} dactyl:group="link path"
58 oncommand="dactyl.clipboardWrite(document.popupNode.getAttribute('path'));"/>
59 <menuitem id="dactyl-context-copy"
60 label={_("mow.contextMenu.copy")} dactyl:group="selection"
62 <menuitem id="dactyl-context-selectall"
63 label={_("mow.contextMenu.selectAll")}
64 command="cmd_selectAll"/>
68 <vbox id={config.ids.commandContainer}>
69 <vbox class="dactyl-container" id="dactyl-multiline-output-container" hidden="false" collapsed="true">
70 <iframe id="dactyl-multiline-output" src="dactyl://content/buffer.xhtml"
71 flex="1" hidden="false" collapsed="false" contextmenu="dactyl-contextmenu"
79 __noSuchMethod__: function (meth, args) Buffer[meth].apply(Buffer, [this.body].concat(args)),
81 get widget() this.widgets.multilineOutput,
82 widgets: Class.Memoize(function widgets() commandline.widgets),
84 body: Class.Memoize(function body() this.widget.contentDocument.documentElement),
85 get document() this.widget.contentDocument,
86 get window() this.widget.contentWindow,
89 * Display a multi-line message.
91 * @param {string} data
92 * @param {string} highlightGroup
94 echo: function echo(data, highlightGroup, silent) {
95 let body = DOM(this.document.body);
97 this.widgets.message = null;
98 if (!commandline.commandVisible)
101 if (modes.main != modes.OUTPUT_MULTILINE) {
102 modes.push(modes.OUTPUT_MULTILINE, null, {
103 onKeyPress: this.closure.onKeyPress,
105 leave: this.closure(function leave(stack) {
107 for (let message in values(this.messages))
109 message.leave(stack);
117 // If it's already XML, assume it knows what it's doing.
118 // Otherwise, white space is significant.
119 // The problem elsewhere is that E4X tends to insert new lines
120 // after interpolated data.
121 XML.ignoreWhitespace = XML.prettyPrinting = false;
123 highlightGroup = "CommandOutput " + (highlightGroup || "");
125 if (isObject(data) && !isinstance(data, _)) {
126 this.lastOutput = null;
128 var output = DOM(<div style="white-space: nowrap" highlight={highlightGroup}/>,
130 data.document = this.document;
132 output.append(data.message);
138 this.messages.push(data);
141 let style = isString(data) ? "pre-wrap" : "nowrap";
142 this.lastOutput = <div style={"white-space: " + style} highlight={highlightGroup}>{data}</div>;
144 var output = DOM(this.lastOutput, this.document);
147 // FIXME: need to make sure an open MOW is closed when commands
148 // that don't generate output are executed
150 this.body.scrollTop = 0;
156 let str = typeof data !== "xml" && data.message || data;
158 dactyl.triggerObserver("echoMultiline", data, highlightGroup, output[0]);
166 click: function onClick(event) {
167 if (event.getPreventDefault())
170 const openLink = function openLink(where) {
171 event.preventDefault();
172 dactyl.open(event.target.href, where);
175 if (event.target instanceof HTMLAnchorElement)
176 switch (DOM.Event.stringify(event)) {
178 openLink(dactyl.CURRENT_TAB);
180 case "<MiddleMouse>":
181 case "<C-LeftMouse>":
182 case "<C-M-LeftMouse>":
183 openLink({ where: dactyl.NEW_TAB, background: true });
185 case "<S-MiddleMouse>":
186 case "<C-S-LeftMouse>":
187 case "<C-M-S-LeftMouse>":
188 openLink({ where: dactyl.NEW_TAB, background: false });
190 case "<S-LeftMouse>":
191 openLink(dactyl.NEW_WINDOW);
195 unload: function onUnload(event) {
196 event.preventDefault();
201 resize: function onResize(event) {
207 popupshowing: function onPopupShowing(event) {
208 let menu = commandline.widgets.contextMenu;
210 link: window.document.popupNode instanceof HTMLAnchorElement,
211 path: window.document.popupNode.hasAttribute("path"),
212 selection: !window.document.commandDispatcher.focusedWindow.getSelection().isCollapsed
215 for (let node in array.iterValues(menu.children)) {
216 let group = node.getAttributeNS(NS, "group");
217 node.hidden = group && !group.split(/\s+/).every(function (g) enabled[g]);
222 onKeyPress: function onKeyPress(eventList) {
223 const KILL = false, PASS = true;
225 if (options["more"] && mow.canScroll(1))
226 this.updateMorePrompt(false, true);
229 events.feedevents(null, eventList);
236 * Changes the height of the message window to fit in the available space.
238 * @param {boolean} open If true, the widget will be opened if it's not
241 resize: function resize(open, extra) {
242 if (!(open || this.visible))
245 let doc = this.widget.contentDocument;
247 let trim = this.spaceNeeded;
248 let availableHeight = config.outputHeight - trim;
250 availableHeight += parseFloat(this.widgets.mowContainer.height || 0);
251 availableHeight -= extra || 0;
253 doc.body.style.minWidth = this.widgets.commandbar.commandline.scrollWidth + "px";
256 let wantedHeight = doc.body.clientHeight;
257 this.widgets.mowContainer.height = Math.min(wantedHeight, availableHeight) + "px",
258 this.wantedHeight = Math.max(0, wantedHeight - availableHeight);
261 this.timeout(adjust);
263 doc.body.style.minWidth = "";
269 if (DOM("#dactyl-bell", document).isVisible)
271 return Math.max(0, DOM("#" + config.ids.commandContainer, document).rect.bottom
272 - window.innerHeight);
276 * Update or remove the multi-line output widget's "MORE" prompt.
278 * @param {boolean} force If true, "-- More --" is shown even if we're
279 * at the end of the output.
280 * @param {boolean} showHelp When true, show the valid key sequences
283 updateMorePrompt: function updateMorePrompt(force, showHelp) {
284 if (!this.visible || !isinstance(modes.main, modes.OUTPUT_MULTILINE))
285 return this.widgets.message = null;
287 let elem = this.widget.contentDocument.documentElement;
290 this.widgets.message = ["MoreMsg", _("mow.moreHelp")];
291 else if (force || (options["more"] && Buffer.canScroll(elem, 1)))
292 this.widgets.message = ["MoreMsg", _("mow.more")];
294 this.widgets.message = ["Question", _("mow.continue")];
297 visible: Modes.boundProperty({
298 get: function get_mowVisible() !this.widgets.mowContainer.collapsed,
299 set: function set_mowVisible(value) {
300 this.widgets.mowContainer.collapsed = !value;
302 let elem = this.widget;
303 if (!value && elem && elem.contentWindow == document.commandDispatcher.focusedWindow) {
305 let focused = content.document.activeElement;
306 if (focused && Events.isInputElement(focused))
309 document.commandDispatcher.focusedWindow = content;
315 modes: function initModes() {
316 modes.addMode("OUTPUT_MULTILINE", {
317 description: "Active when the multi-line output buffer is open",
318 bases: [modes.NORMAL]
321 mappings: function initMappings() {
326 mappings.add([modes.COMMAND],
327 ["g<lt>"], "Redisplay the last command output",
329 dactyl.assert(mow.lastOutput, _("mow.noPreviousOutput"));
330 mow.echo(mow.lastOutput, "Normal");
333 mappings.add([modes.OUTPUT_MULTILINE],
335 "Return to the previous mode",
336 function () { modes.pop(null, { fromEscape: true }); });
338 let bind = function bind(keys, description, action, test, default_) {
339 mappings.add([modes.OUTPUT_MULTILINE],
342 if (!options["more"])
344 else if (test && !test(args))
347 res = action.call(this, args);
349 if (res === PASS || res === DROP)
352 mow.updateMorePrompt();
355 else if (res === PASS)
356 events.feedkeys(args.command);
358 count: action.length > 0
362 bind(["j", "<C-e>", "<Down>"], "Scroll down one line",
363 function ({ count }) { mow.scrollVertical("lines", 1 * (count || 1)); },
364 function () mow.canScroll(1), BEEP);
366 bind(["k", "<C-y>", "<Up>"], "Scroll up one line",
367 function ({ count }) { mow.scrollVertical("lines", -1 * (count || 1)); },
368 function () mow.canScroll(-1), BEEP);
370 bind(["<C-j>", "<C-m>", "<Return>"], "Scroll down one line, exit on last line",
371 function ({ count }) { mow.scrollVertical("lines", 1 * (count || 1)); },
372 function () mow.canScroll(1), DROP);
375 bind(["<C-d>"], "Scroll down half a page",
376 function ({ count }) { mow.scrollVertical("pages", .5 * (count || 1)); },
377 function () mow.canScroll(1), BEEP);
379 bind(["<C-f>", "<PageDown>"], "Scroll down one page",
380 function ({ count }) { mow.scrollVertical("pages", 1 * (count || 1)); },
381 function () mow.canScroll(1), BEEP);
383 bind(["<Space>"], "Scroll down one page",
384 function ({ count }) { mow.scrollVertical("pages", 1 * (count || 1)); },
385 function () mow.canScroll(1), DROP);
387 bind(["<C-u>"], "Scroll up half a page",
388 function ({ count }) { mow.scrollVertical("pages", -.5 * (count || 1)); },
389 function () mow.canScroll(-1), BEEP);
391 bind(["<C-b>", "<PageUp>"], "Scroll up half a page",
392 function ({ count }) { mow.scrollVertical("pages", -1 * (count || 1)); },
393 function () mow.canScroll(-1), BEEP);
395 bind(["gg"], "Scroll to the beginning of output",
396 function () { mow.scrollToPercent(null, 0); });
398 bind(["G"], "Scroll to the end of output",
399 function ({ count }) { mow.scrollToPercent(null, count || 100); });
401 // copy text to clipboard
402 bind(["<C-y>"], "Yank selection to clipboard",
403 function () { dactyl.clipboardWrite(buffer.getCurrentWord(mow.window)); });
406 bind(["q"], "Close the output window",
408 function () false, DROP);
410 options: function initOptions() {
411 options.add(["more"],
412 "Pause the message list window when the full output will not fit on one page",
417 // vim: set fdm=marker sw=4 ts=4 et: