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-2012 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 overlay.overlayWindow(window, {
49 ["window", { id: document.documentElement.id, xmlns: "xul" },
51 ["menupopup", { id: "dactyl-contextmenu", highlight: "Events", events: "contextEvents" },
52 ["menuitem", { id: "dactyl-context-copylink", label: _("mow.contextMenu.copyLink"),
53 "dactyl:group": "link", oncommand: "goDoCommand('cmd_copyLink');" }],
54 ["menuitem", { id: "dactyl-context-copypath", label: _("mow.contextMenu.copyPath"),
55 "dactyl:group": "link path", oncommand: "dactyl.clipboardWrite(document.popupNode.getAttribute('path'));" }],
56 ["menuitem", { id: "dactyl-context-copy", label: _("mow.contextMenu.copy"),
57 "dactyl:group": "selection", command: "cmd_copy" }],
58 ["menuitem", { id: "dactyl-context-selectall", label: _("mow.contextMenu.selectAll"),
59 command: "cmd_selectAll" }]]]],
61 ["vbox", { id: config.ids.commandContainer, xmlns: "xul" },
62 ["vbox", { class: "dactyl-container", id: "dactyl-multiline-output-container", hidden: "false", collapsed: "true" },
63 ["iframe", { id: "dactyl-multiline-output", src: "dactyl://content/buffer.xhtml",
64 flex: "1", hidden: "false", collapsed: "false",
65 contextmenu: "dactyl-contextmenu", highlight: "Events" }]]]]
69 __noSuchMethod__: function (meth, args) Buffer[meth].apply(Buffer, [this.body].concat(args)),
71 get widget() this.widgets.multilineOutput,
72 widgets: Class.Memoize(function widgets() commandline.widgets),
74 body: Class.Memoize(function body() this.widget.contentDocument.documentElement),
75 get document() this.widget.contentDocument,
76 get window() this.widget.contentWindow,
79 * Display a multi-line message.
81 * @param {string} data
82 * @param {string} highlightGroup
84 echo: function echo(data, highlightGroup, silent) {
85 let body = DOM(this.document.body);
87 this.widgets.message = null;
88 if (!commandline.commandVisible)
91 if (modes.main != modes.OUTPUT_MULTILINE) {
92 modes.push(modes.OUTPUT_MULTILINE, null, {
93 onKeyPress: this.closure.onKeyPress,
95 leave: this.closure(function leave(stack) {
97 for (let message in values(this.messages))
107 highlightGroup = "CommandOutput " + (highlightGroup || "");
109 if (isObject(data) && !isinstance(data, _) && !DOM.isJSONXML(data)) {
110 this.lastOutput = null;
112 var output = DOM(["div", { style: "white-space: nowrap", highlight: highlightGroup }],
114 data.document = this.document;
116 output.append(data.message);
122 this.messages.push(data);
125 let style = isString(data) ? "pre-wrap" : "nowrap";
126 this.lastOutput = ["div", { style: "white-space: " + style, highlight: highlightGroup },
129 var output = DOM(this.lastOutput, this.document);
132 // FIXME: need to make sure an open MOW is closed when commands
133 // that don't generate output are executed
135 this.body.scrollTop = 0;
141 let str = typeof data !== "xml" && data.message || data;
143 dactyl.triggerObserver("echoMultiline", data, highlightGroup, output[0]);
151 click: function onClick(event) {
152 if (event.getPreventDefault())
155 const openLink = function openLink(where) {
156 event.preventDefault();
157 dactyl.open(event.target.href, where);
160 if (event.target instanceof HTMLAnchorElement)
161 switch (DOM.Event.stringify(event)) {
163 openLink(dactyl.CURRENT_TAB);
165 case "<MiddleMouse>":
166 case "<C-LeftMouse>":
167 case "<C-M-LeftMouse>":
168 openLink({ where: dactyl.NEW_TAB, background: true });
170 case "<S-MiddleMouse>":
171 case "<C-S-LeftMouse>":
172 case "<C-M-S-LeftMouse>":
173 openLink({ where: dactyl.NEW_TAB, background: false });
175 case "<S-LeftMouse>":
176 openLink(dactyl.NEW_WINDOW);
180 unload: function onUnload(event) {
181 event.preventDefault();
186 resize: function onResize(event) {
192 popupshowing: function onPopupShowing(event) {
193 let menu = commandline.widgets.contextMenu;
195 link: window.document.popupNode instanceof HTMLAnchorElement,
196 path: window.document.popupNode.hasAttribute("path"),
197 selection: !window.document.commandDispatcher.focusedWindow.getSelection().isCollapsed
200 for (let node in array.iterValues(menu.children)) {
201 let group = node.getAttributeNS(NS, "group");
202 node.hidden = group && !group.split(/\s+/).every(function (g) enabled[g]);
207 onKeyPress: function onKeyPress(eventList) {
208 const KILL = false, PASS = true;
210 if (options["more"] && mow.canScroll(1))
211 this.updateMorePrompt(false, true);
214 events.feedevents(null, eventList);
221 * Changes the height of the message window to fit in the available space.
223 * @param {boolean} open If true, the widget will be opened if it's not
226 resize: function resize(open, extra) {
227 if (!(open || this.visible))
230 let doc = this.widget.contentDocument;
232 let trim = this.spaceNeeded;
233 let availableHeight = config.outputHeight - trim;
235 availableHeight += parseFloat(this.widgets.mowContainer.height || 0);
236 availableHeight -= extra || 0;
238 doc.body.style.minWidth = this.widgets.commandbar.commandline.scrollWidth + "px";
241 let wantedHeight = doc.body.clientHeight;
242 this.widgets.mowContainer.height = Math.min(wantedHeight, availableHeight) + "px",
243 this.wantedHeight = Math.max(0, wantedHeight - availableHeight);
246 this.timeout(adjust);
248 doc.body.style.minWidth = "";
254 if (DOM("#dactyl-bell", document).isVisible)
256 return Math.max(0, DOM("#" + config.ids.commandContainer, document).rect.bottom
257 - window.innerHeight);
261 * Update or remove the multi-line output widget's "MORE" prompt.
263 * @param {boolean} force If true, "-- More --" is shown even if we're
264 * at the end of the output.
265 * @param {boolean} showHelp When true, show the valid key sequences
268 updateMorePrompt: function updateMorePrompt(force, showHelp) {
269 if (!this.visible || !isinstance(modes.main, modes.OUTPUT_MULTILINE))
270 return this.widgets.message = null;
272 let elem = this.widget.contentDocument.documentElement;
275 this.widgets.message = ["MoreMsg", _("mow.moreHelp")];
276 else if (force || (options["more"] && Buffer.canScroll(elem, 1)))
277 this.widgets.message = ["MoreMsg", _("mow.more")];
279 this.widgets.message = ["Question", _("mow.continue")];
282 visible: Modes.boundProperty({
283 get: function get_mowVisible() !this.widgets.mowContainer.collapsed,
284 set: function set_mowVisible(value) {
285 this.widgets.mowContainer.collapsed = !value;
287 let elem = this.widget;
288 if (!value && elem && elem.contentWindow == document.commandDispatcher.focusedWindow) {
290 let focused = content.document.activeElement;
291 if (focused && Events.isInputElement(focused))
294 document.commandDispatcher.focusedWindow = content;
300 modes: function initModes() {
301 modes.addMode("OUTPUT_MULTILINE", {
302 description: "Active when the multi-line output buffer is open",
303 bases: [modes.NORMAL]
306 mappings: function initMappings() {
311 mappings.add([modes.COMMAND],
312 ["g<lt>"], "Redisplay the last command output",
314 dactyl.assert(mow.lastOutput, _("mow.noPreviousOutput"));
315 mow.echo(mow.lastOutput, "Normal");
318 mappings.add([modes.OUTPUT_MULTILINE],
320 "Return to the previous mode",
321 function () { modes.pop(null, { fromEscape: true }); });
323 let bind = function bind(keys, description, action, test, default_) {
324 mappings.add([modes.OUTPUT_MULTILINE],
327 if (!options["more"])
329 else if (test && !test(args))
332 res = action.call(this, args);
334 if (res === PASS || res === DROP)
337 mow.updateMorePrompt();
340 else if (res === PASS)
341 events.feedkeys(args.command);
343 count: action.length > 0
347 bind(["j", "<C-e>", "<Down>"], "Scroll down one line",
348 function ({ count }) { mow.scrollVertical("lines", 1 * (count || 1)); },
349 function () mow.canScroll(1), BEEP);
351 bind(["k", "<C-y>", "<Up>"], "Scroll up one line",
352 function ({ count }) { mow.scrollVertical("lines", -1 * (count || 1)); },
353 function () mow.canScroll(-1), BEEP);
355 bind(["<C-j>", "<C-m>", "<Return>"], "Scroll down one line, exit on last line",
356 function ({ count }) { mow.scrollVertical("lines", 1 * (count || 1)); },
357 function () mow.canScroll(1), DROP);
360 bind(["<C-d>"], "Scroll down half a page",
361 function ({ count }) { mow.scrollVertical("pages", .5 * (count || 1)); },
362 function () mow.canScroll(1), BEEP);
364 bind(["<C-f>", "<PageDown>"], "Scroll down one page",
365 function ({ count }) { mow.scrollVertical("pages", 1 * (count || 1)); },
366 function () mow.canScroll(1), BEEP);
368 bind(["<Space>"], "Scroll down one page",
369 function ({ count }) { mow.scrollVertical("pages", 1 * (count || 1)); },
370 function () mow.canScroll(1), DROP);
372 bind(["<C-u>"], "Scroll up half a page",
373 function ({ count }) { mow.scrollVertical("pages", -.5 * (count || 1)); },
374 function () mow.canScroll(-1), BEEP);
376 bind(["<C-b>", "<PageUp>"], "Scroll up half a page",
377 function ({ count }) { mow.scrollVertical("pages", -1 * (count || 1)); },
378 function () mow.canScroll(-1), BEEP);
380 bind(["gg"], "Scroll to the beginning of output",
381 function () { mow.scrollToPercent(null, 0); });
383 bind(["G"], "Scroll to the end of output",
384 function ({ count }) { mow.scrollToPercent(null, count || 100); });
386 // copy text to clipboard
387 bind(["<C-y>"], "Yank selection to clipboard",
388 function () { dactyl.clipboardWrite(Buffer.currentWord(mow.window)); });
391 bind(["q"], "Close the output window",
393 function () false, DROP);
395 options: function initOptions() {
396 options.add(["more"],
397 "Pause the message list window when the full output will not fit on one page",
402 // vim: set fdm=marker sw=4 ts=4 et: