1
0
mirror of https://github.com/gryf/pentadactyl-pm.git synced 2026-03-08 11:15:46 +01:00

Major documentation updates and formatting fixes, and many, many other changes thanks to an MQ glitch, including:

* Significant completion speed improvements
 * Significantly improve startup speed, in large part by lazily
   instantiating Options and Commands, lazily installing highlight
   stylesheets, etc.
 * Update logos and icons, fix atrocious about page
 * Fix Teledactyl
 * JavaScript completion now avoids accessing property values
 * Add Option#persist to define which options are saved with :mkp
 * Add new Dactyl component which holds add-on-specific configuration
   information and removes need for separate components for each dactyl
   host
 * Several fixes for latest nightlies
 * Significant code cleanup and many bug fixes

--HG--
rename : muttator/AUTHORS => teledactyl/AUTHORS
rename : muttator/Donors => teledactyl/Donors
rename : muttator/Makefile => teledactyl/Makefile
rename : muttator/NEWS => teledactyl/NEWS
rename : muttator/TODO => teledactyl/TODO
rename : muttator/chrome.manifest => teledactyl/chrome.manifest
rename : muttator/components/commandline-handler.js => teledactyl/components/commandline-handler.js
rename : muttator/components/protocols.js => teledactyl/components/protocols.js
rename : muttator/content/addressbook.js => teledactyl/content/addressbook.js
rename : muttator/content/compose/compose.js => teledactyl/content/compose/compose.js
rename : muttator/content/compose/compose.xul => teledactyl/content/compose/compose.xul
rename : muttator/content/compose/dactyl.dtd => teledactyl/content/compose/dactyl.dtd
rename : muttator/content/compose/dactyl.xul => teledactyl/content/compose/dactyl.xul
rename : muttator/content/config.js => teledactyl/content/config.js
rename : muttator/content/dactyl.dtd => teledactyl/content/dactyl.dtd
rename : muttator/content/logo.png => teledactyl/content/logo.png
rename : muttator/content/mail.js => teledactyl/content/mail.js
rename : muttator/content/muttator.xul => teledactyl/content/pentadactyl.xul
rename : muttator/contrib/vim/Makefile => teledactyl/contrib/vim/Makefile
rename : muttator/contrib/vim/ftdetect/muttator.vim => teledactyl/contrib/vim/ftdetect/muttator.vim
rename : muttator/contrib/vim/mkvimball.txt => teledactyl/contrib/vim/mkvimball.txt
rename : muttator/contrib/vim/syntax/muttator.vim => teledactyl/contrib/vim/syntax/muttator.vim
rename : muttator/install.rdf => teledactyl/install.rdf
rename : muttator/locale/en-US/Makefile => teledactyl/locale/en-US/Makefile
rename : muttator/locale/en-US/all.xml => teledactyl/locale/en-US/all.xml
rename : muttator/locale/en-US/autocommands.xml => teledactyl/locale/en-US/autocommands.xml
rename : muttator/locale/en-US/gui.xml => teledactyl/locale/en-US/gui.xml
rename : muttator/locale/en-US/intro.xml => teledactyl/locale/en-US/intro.xml
rename : muttator/skin/icon.png => teledactyl/skin/icon.png
This commit is contained in:
Kris Maglione
2010-09-17 06:21:33 -04:00
parent bfbb4b1313
commit 1557b70f45
125 changed files with 4409 additions and 3969 deletions

View File

@@ -0,0 +1,151 @@
// Copyright (c) 2008 by Christian Dietrich <stettberger@dokucode.de>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
const Addressbook = Module("addressbook", {
init: function () {
},
// TODO: add option for a format specifier, like:
// :set displayname=%l, %f
generateDisplayName: function (firstName, lastName) {
if (firstName && lastName)
return lastName + ", " + firstName;
else if (firstName)
return firstName;
else if (lastName)
return lastName;
else
return "";
},
getDirectoryFromURI: function (uri) services.get("rdf").GetResource(uri).QueryInterface(Ci.nsIAbDirectory),
add: function (address, firstName, lastName, displayName) {
const personalAddressbookURI = "moz-abmdbdirectory://abook.mab";
let directory = this.getDirectoryFromURI(personalAddressbookURI);
let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance(Ci.nsIAbCard);
if (!address || !directory || !card)
return false;
card.primaryEmail = address;
card.firstName = firstName;
card.lastName = lastName;
card.displayName = displayName;
return directory.addCard(card);
},
// TODO: add telephone number support
list: function (filter, newMail) {
let addresses = [];
let dirs = abManager.directories;
let lowerFilter = filter.toLowerCase();
while (dirs.hasMoreElements()) {
let addrbook = dirs.getNext().QueryInterface(Ci.nsIAbDirectory);
let cards = addrbook.childCards;
while (cards.hasMoreElements()) {
let card = cards.getNext().QueryInterface(Ci.nsIAbCard);
//var mail = card.primaryEmail || ""; //XXX
let displayName = card.displayName;
if (!displayName)
displayName = this.generateDisplayName(card.firstName, card.lastName);
if (displayName.toLowerCase().indexOf(lowerFilter) > -1
|| card.primaryEmail.toLowerCase().indexOf(lowerFilter) > -1)
addresses.push([displayName, card.primaryEmail]);
}
}
if (addresses.length < 1) {
if (!filter)
dactyl.echoerr("Exxx: No contacts", commandline.FORCE_SINGLELINE);
else
dactyl.echoerr("Exxx: No contacts matching string '" + filter + "'", commandline.FORCE_SINGLELINE);
return false;
}
if (newMail) {
// Now we have to create a new message
let args = {};
args.to = addresses.map(
function (address) "\"" + address[0].replace(/"/g, "") + " <" + address[1] + ">\""
).join(", ");
mail.composeNewMail(args);
}
else {
let list = template.tabular(["Name", "Address"], [],
[[util.clip(address[0], 50), address[1]] for ([, address] in Iterator(addresses))]
);
commandline.echo(list, commandline.HL_NORMAL, commandline.FORCE_MULTILINE);
}
return true;
}
}, {
}, {
commands: function () {
commands.add(["con[tact]"],
"Add an address book entry",
function (args) {
let mailAddr = args[0]; // TODO: support more than one email address
let firstName = args["-firstname"] || null;
let lastName = args["-lastname"] || null;
let displayName = args["-name"] || null;
if (!displayName)
displayName = this.generateDisplayName(firstName, lastName);
if (addressbook.add(mailAddr, firstName, lastName, displayName))
dactyl.echomsg("Added address: " + displayName + " <" + mailAddr + ">", 1, commandline.FORCE_SINGLELINE);
else
dactyl.echoerr("Exxx: Could not add contact `" + mailAddr + "'", commandline.FORCE_SINGLELINE);
},
{
argCount: "+",
options: [[["-firstname", "-f"], commands.OPTION_STRING],
[["-lastname", "-l"], commands.OPTION_STRING],
[["-name", "-n"], commands.OPTION_STRING]]
});
commands.add(["contacts", "addr[essbook]"],
"List or open multiple addresses",
function (args) { addressbook.list(args.string, args.bang); },
{ bang: true });
},
mappings: function () {
var myModes = config.mailModes;
mappings.add(myModes, ["a"],
"Open a prompt to save a new addressbook entry for the sender of the selected message",
function () {
try {
var to = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor;
}
catch (e) {
dactyl.beep();
}
if (!to)
return;
let address = to.substring(to.indexOf("<") + 1, to.indexOf(">"));
let displayName = to.substr(0, to.indexOf("<") - 1);
if (/^\S+\s+\S+\s*$/.test(displayName)) {
let names = displayName.split(/\s+/);
displayName = "-firstname=" + names[0].replace(/"/g, "")
+ " -lastname=" + names[1].replace(/"/g, "");
}
else
displayName = "-name=\"" + displayName.replace(/"/g, "") + "\"";
commandline.open(":", "contact " + address + " " + displayName, modes.EX);
});
}
});
// vim: set fdm=marker sw=4 ts=4 et:

View File

@@ -0,0 +1,103 @@
// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
function Compose() { //{{{
////////////////////////////////////////////////////////////////////////////////
////////////////////// PRIVATE SECTION /////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
config.features = ["addressbook"]; // the composer has no special features
var stateListener = {
QueryInterface: function (id) {
if (id.equals(Ci.nsIDocumentStateListener))
return this;
throw Cr.NS_NOINTERFACE;
},
// this is (also) fired once the new compose window loaded the message for the first time
NotifyDocumentStateChanged: function (nowDirty) {
// only edit with external editor if this window was not cached!
if (options["autoexternal"] && !window.messageWasEditedExternally/* && !gMsgCompose.recycledWindow*/) {
window.messageWasEditedExternally = true;
editor.editFieldExternally();
}
},
NotifyDocumentCreated: function () {},
NotifyDocumentWillBeDestroyed: function () {}
};
// XXX: Hack!
window.document.addEventListener("load", function () {
if (window.messageWasEditedExternally === undefined) {
window.messageWasEditedExternally = false;
GetCurrentEditor().addDocumentStateListener(stateListener);
}
}, true);
window.addEventListener("compose-window-close", function () {
window.messageWasEditedExternally = false;
}, true);
/*window.document.addEventListener("unload", function () {
GetCurrentEditor().removeDocumentStateListener(config.stateListener);
}, true);*/
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// OPTIONS /////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// MAPPINGS ////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// COMMANDS ////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
mappings.add([modes.COMPOSE],
["e"], "Edit message",
function () { editor.editFieldExternally(); });
mappings.add([modes.COMPOSE],
["y"], "Send message now",
function () { window.goDoCommand("cmd_sendNow"); });
mappings.add([modes.COMPOSE],
["Y"], "Send message later",
function () { window.goDoCommand("cmd_sendLater"); });
// FIXME: does not really work reliably
mappings.add([modes.COMPOSE],
["t"], "Select To: field",
function () { awSetFocus(0, awGetInputElement(1)); });
mappings.add([modes.COMPOSE],
["s"], "Select Subject: field",
function () { GetMsgSubjectElement().focus(); });
mappings.add([modes.COMPOSE],
["i"], "Select message body",
function () { SetMsgBodyFrameFocus(); });
mappings.add([modes.COMPOSE],
["q"], "Close composer, ask when for unsaved changes",
function () { DoCommandClose(); });
mappings.add([modes.COMPOSE],
["Q", "ZQ"], "Force closing composer",
function () { MsgComposeCloseWindow(true); /* cache window for better performance*/ });
/////////////////////////////////////////////////////////////////////////////}}}
////////////////////// PUBLIC SECTION //////////////////////////////////////////
/////////////////////////////////////////////////////////////////////////////{{{
return {};
//}}}
} //}}}
// vim: set fdm=marker sw=4 ts=4 et:

View File

@@ -0,0 +1,19 @@
<?xml version="1.0"?>
<!-- ***** BEGIN LICENSE BLOCK ***** {{{
Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
This work is licensed for reuse under an MIT license. Details are
given in the LICENSE.txt file included with this file.
}}} ***** END LICENSE BLOCK ***** -->
<!--?xml-stylesheet href="chrome://browser/skin/" type="text/css"?-->
<overlay id="muttator"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:nc="http://home.netscape.com/NC-rdf#"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
</overlay>
<!-- vim: set fdm=marker sw=4 ts=4 et: -->

View File

@@ -0,0 +1,4 @@
<!ENTITY dactyl.mainWindow "msgcomposeWindow">
<!ENTITY dactyl.name "muttator">
<!ENTITY dactyl.statusBefore "">
<!ENTITY dactyl.statusAfter "statusText">

View File

@@ -0,0 +1,98 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- ***** BEGIN LICENSE BLOCK ***** {{{
// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
}}} ***** END LICENSE BLOCK ***** -->
<?xml-stylesheet href="chrome://dactyl/skin/dactyl.css" type="text/css"?>
<!DOCTYPE overlay SYSTEM "dactyl.dtd" [
<!ENTITY dactyl.content "chrome://dactyl/content/">
]>
<overlay id="dactyl"
xmlns:dactyl="http://vimperator.org/namespaces/liberator"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:nc="http://home.netscape.com/NC-rdf#"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/x-javascript;version=1.8" src="&dactyl.content;dactyl-overlay.js"/>
<window id="&dactyl.mainWindow;">
<keyset id="mainKeyset">
<key id="key_open_vimbar" key=":" oncommand="dactyl.modules.commandline.open(':', '', dactyl.modules.modes.EX);" modifiers=""/>
<key id="key_stop" keycode="VK_ESCAPE" oncommand="dactyl.modules.events.onEscape();"/>
<!-- other keys are handled inside the event loop in events.js -->
</keyset>
<popupset>
<panel id="dactyl-visualbell" dactyl:highlight="Bell"/>
</popupset>
<!--this notifies us also of focus events in the XUL
from: http://developer.mozilla.org/en/docs/XUL_Tutorial:Updating_Commands !-->
<commandset id="onPentadactylFocus"
commandupdater="true"
events="focus"
oncommandupdate="if (dactyl.modules.events != undefined) dactyl.modules.events.onFocusChange(event);"/>
<commandset id="onPentadactylSelect"
commandupdater="true"
events="select"
oncommandupdate="if (dactyl.modules.events != undefined) dactyl.modules.events.onSelectionChange(event);"/>
<!-- As of Firefox 3.1pre, <iframe>.height changes do not seem to have immediate effect,
therefore we need to put them into a <vbox> for which that works just fine -->
<vbox class="dactyl-container" hidden="false" collapsed="true">
<iframe id="dactyl-multiline-output" src="chrome://dactyl/content/buffer.xhtml"
flex="1" hidden="false" collapsed="false"
onclick="dactyl.modules.commandline.onMultilineOutputEvent(event)"/>
</vbox>
<vbox class="dactyl-container" hidden="false" collapsed="true">
<iframe id="dactyl-completions" src="chrome://dactyl/content/buffer.xhtml"
flex="1" hidden="false" collapsed="false"
onclick="dactyl.modules.commandline.onMultilineOutputEvent(event)"/>
</vbox>
<stack orient="horizontal" align="stretch" class="dactyl-container" dactyl:highlight="CmdLine">
<textbox class="plain" id="dactyl-message" flex="1" readonly="true" dactyl:highlight="Normal"/>
<hbox id="dactyl-commandline" hidden="false" collapsed="true" class="dactyl-container" dactyl:highlight="Normal">
<label class="plain" id="dactyl-commandline-prompt" flex="0" crop="end" value="" collapsed="true"/>
<textbox class="plain" id="dactyl-commandline-command" flex="1" type="timed" timeout="100"
oninput="dactyl.modules.commandline.onEvent(event);"
onkeyup="dactyl.modules.commandline.onEvent(event);"
onfocus="dactyl.modules.commandline.onEvent(event);"
onblur="dactyl.modules.commandline.onEvent(event);"/>
</hbox>
</stack>
<vbox class="dactyl-container" hidden="false" collapsed="false">
<textbox id="dactyl-multiline-input" class="plain" flex="1" rows="1" hidden="false" collapsed="true" multiline="true"
onkeypress="dactyl.modules.commandline.onMultilineInputEvent(event);"
oninput="dactyl.modules.commandline.onMultilineInputEvent(event);"
onblur="dactyl.modules.commandline.onMultilineInputEvent(event);"/>
</vbox>
</window>
<statusbar id="status-bar" dactyl:highlight="StatusLine">
<hbox insertbefore="&dactyl.statusBefore;" insertafter="&dactyl.statusAfter;"
id="dactyl-statusline" flex="1" hidden="false" align="center">
<textbox class="plain" id="dactyl-statusline-field-url" readonly="false" flex="1" crop="end"/>
<label class="plain" id="dactyl-statusline-field-inputbuffer" flex="0"/>
<label class="plain" id="dactyl-statusline-field-progress" flex="0"/>
<label class="plain" id="dactyl-statusline-field-tabcount" flex="0"/>
<label class="plain" id="dactyl-statusline-field-bufferposition" flex="0"/>
</hbox>
<!-- just hide them since other elements expect them -->
<statusbarpanel id="statusbar-display" hidden="true"/>
<statusbarpanel id="statusbar-progresspanel" hidden="true"/>
</statusbar>
</overlay>
<!-- vim: set fdm=marker sw=4 ts=4 et: -->

View File

@@ -0,0 +1,179 @@
// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
const Config = Module("config", ConfigBase, {
init: function init() {
init.supercall(this);
// don't wait too long when selecting new messages
// GetThreadTree()._selectDelay = 300; // TODO: make configurable
},
autocommands: {
DOMLoad: "Triggered when a page's DOM content has fully loaded",
FolderLoad: "Triggered after switching folders in Thunderbird",
PageLoadPre: "Triggered after a page load is initiated",
PageLoad: "Triggered when a page gets (re)loaded/opened",
Enter: "Triggered after Thunderbird starts",
Leave: "Triggered before exiting Thunderbird",
LeavePre: "Triggered before exiting Thunderbird",
},
get browser() getBrowser(),
dialogs: {
about: ["About Thunderbird",
function () { window.openAboutDialog(); }],
addons: ["Manage Add-ons",
function () { window.openAddonsMgr(); }],
addressbook: ["Address book",
function () { window.toAddressBook(); }],
checkupdates: ["Check for updates",
function () { window.checkForUpdates(); }],
console: ["JavaScript console",
function () { window.toJavaScriptConsole(); }],
dominspector: ["DOM Inspector",
function () { window.inspectDOMDocument(content.document); }],
downloads: ["Manage Downloads",
function () { window.toOpenWindowByType('Download:Manager', 'chrome://mozapps/content/downloads/downloads.xul', 'chrome,dialog=no,resizable'); }],
preferences: ["Show Thunderbird preferences dialog",
function () { openOptionsDialog(); }],
printsetup: ["Setup the page size and orientation before printing",
function () { PrintUtils.showPageSetup(); }],
print: ["Show print dialog",
function () { PrintUtils.print(); }],
saveframe: ["Save frame to disk",
function () { window.saveFrameDocument(); }],
savepage: ["Save page to disk",
function () { window.saveDocument(window.content.document); }],
},
defaults: {
guioptions: "frb",
showtabline: 1,
titlestring: "Teledactyl"
},
/*** optional options, there are checked for existence and a fallback provided ***/
features: ["hints", "mail", "marks", "addressbook", "tabs"],
focusChange: function (win) {
// we switch to -- MESSAGE -- mode for Teledactyl when the main HTML widget gets focus
if (win && win.document instanceof HTMLDocument || dactyl.focus instanceof HTMLAnchorElement) {
if (config.isComposeWindow)
modes.set(modes.INSERT, modes.TEXTAREA);
else if (dactyl.mode != modes.MESSAGE)
dactyl.mode = modes.MESSAGE;
}
},
guioptions: {
m: ["MenuBar", ["mail-toolbar-menubar2"]],
T: ["Toolbar" , ["mail-bar2"]],
f: ["Folder list", ["folderPaneBox", "folderpane_splitter"]],
F: ["Folder list header", ["folderPaneHeader"]]
},
// they are sorted by relevance, not alphabetically
helpFiles: ["intro.html", "version.html"],
get isComposeWindow() window.wintype == "msgcompose",
get mainWidget() this.isComposeWindow ? document.getElementById("content-frame") : GetThreadTree(),
get mainWindowId() this.isComposeWindow ? "msgcomposeWindow" : "messengerWindow",
modes: [
["MESSAGE", { char: "m" }],
["COMPOSE"]
],
get browserModes() [modes.MESSAGE],
get mailModes() [modes.NORMAL],
// NOTE: as I don't use TB I have no idea how robust this is. --djk
get outputHeight() {
if (!this.isComposeWindow) {
let container = document.getElementById("tabpanelcontainer").boxObject;
let deck = document.getElementById("displayDeck");
let box = document.getElementById("messagepanebox");
let splitter = document.getElementById("threadpane-splitter").boxObject;
if (splitter.width > splitter.height)
return container.height - deck.minHeight - box.minHeight- splitter.height;
else
return container.height - Math.max(deck.minHeight, box.minHeight);
}
else
return document.getElementById("appcontent").boxObject.height;
},
removeTab: function (tab) {
if (config.tabbrowser.mTabs.length > 1)
config.tabbrowser.removeTab(tab);
else
dactyl.beep();
},
get scripts() this.isComposeWindow ? ["compose/compose.js"] : [
"addressbook",
"mail",
"tabs",
],
styleableChrome: ["chrome://messenger/content/messenger.xul",
"chrome://messenger/content/messengercompose/messengercompose.xul"],
tabbrowser: {
__proto__: document.getElementById("tabmail"),
get mTabContainer() this.tabContainer,
get mTabs() this.tabContainer.childNodes,
get mCurrentTab() this.tabContainer.selectedItem,
get mStrip() this.tabStrip,
get browsers() [browser for (browser in Iterator(this.mTabs))]
},
// to allow Vim to :set ft=mail automatically
tempFile: "mutt-ator-mail",
get visualbellWindow() document.getElementById(this.mainWindowId),
}, {
}, {
commands: function () {
commands.add(["pref[erences]", "prefs"],
"Show " + config.host + " preferences",
function () { window.openOptionsDialog(); },
{ argCount: "0" });
},
modes: function () {
this.ignoreKeys = {
"<Return>": modes.NORMAL | modes.INSERT,
"<Space>": modes.NORMAL | modes.INSERT,
"<Up>": modes.NORMAL | modes.INSERT,
"<Down>": modes.NORMAL | modes.INSERT
};
},
optons: function () {
// FIXME: comment obviously incorrect
// 0: never automatically edit externally
// 1: automatically edit externally when message window is shown the first time
// 2: automatically edit externally, once the message text gets focus (not working currently)
options.add(["autoexternal", "ae"],
"Edit message with external editor by default",
"boolean", false);
options.add(["online"],
"Set the 'work offline' option",
"boolean", true,
{
setter: function (value) {
if (MailOfflineMgr.isOnline() != value)
MailOfflineMgr.toggleOfflineStatus();
return value;
},
getter: function () MailOfflineMgr.isOnline()
});
}
});
// vim: set fdm=marker sw=4 ts=4 et:

View File

@@ -0,0 +1,13 @@
<!ENTITY % dactylBranding SYSTEM "chrome://branding/locale/brand.dtd">
%dactylBranding;
<!ENTITY dactyl.mainWindow "messengerWindow">
<!ENTITY dactyl.name "teledactyl">
<!ENTITY dactyl.idname "TELEDACTYL">
<!ENTITY dactyl.appname "Teledactyl">
<!ENTITY dactyl.host "&brandShortName;">
<!ENTITY dactyl.hostbin "thunderbird">
<!ENTITY dactyl.statusBefore "">
<!ENTITY dactyl.statusAfter "statusTextBox">

BIN
teledactyl/content/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

943
teledactyl/content/mail.js Normal file
View File

@@ -0,0 +1,943 @@
// Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
const Mail = Module("mail", {
init: function () {
services.add("smtpService", "@mozilla.org/messengercompose/smtp;1", Ci.nsISmtpService);
// used for asynchronously selecting messages after wrapping folders
this._selectMessageKeys = [];
this._selectMessageCount = 1;
this._selectMessageReverse = false;
this._mailSession = Cc["@mozilla.org/messenger/services/session;1"].getService(Ci.nsIMsgMailSession);
this._notifyFlags = Ci.nsIFolderListener.intPropertyChanged | Ci.nsIFolderListener.event;
this._mailSession.AddFolderListener(this._folderListener, this._notifyFlags);
},
_folderListener: {
OnItemAdded: function (parentItem, item) {},
OnItemRemoved: function (parentItem, item) {},
OnItemPropertyChanged: function (item, property, oldValue, newValue) {},
OnItemIntPropertyChanged: function (item, property, oldValue, newValue) {},
OnItemBoolPropertyChanged: function (item, property, oldValue, newValue) {},
OnItemUnicharPropertyChanged: function (item, property, oldValue, newValue) {},
OnItemPropertyFlagChanged: function (item, property, oldFlag, newFlag) {},
OnItemEvent: function (folder, event) {
let eventType = event.toString();
if (eventType == "FolderLoaded") {
if (folder) {
let msgFolder = folder.QueryInterface(Ci.nsIMsgFolder);
autocommands.trigger("FolderLoaded", { url: msgFolder });
// Jump to a message when requested
let indices = [];
if (mail._selectMessageKeys.length > 0) {
for (let j = 0; j < mail._selectMessageKeys.length; j++)
indices.push([gDBView.findIndexFromKey(mail._selectMessageKeys[j], true), mail._selectMessageKeys[j]]);
indices.sort();
let index = mail._selectMessageCount - 1;
if (mail._selectMessageReverse)
index = mail._selectMessageKeys.length - 1 - index;
gDBView.selectMsgByKey(indices[index][1]);
mail._selectMessageKeys = [];
}
}
}
/*else if (eventType == "ImapHdrDownloaded") {}
else if (eventType == "DeleteOrMoveMsgCompleted") {}
else if (eventType == "DeleteOrMoveMsgFailed") {}
else if (eventType == "AboutToCompact") {}
else if (eventType == "CompactCompleted") {}
else if (eventType == "RenameCompleted") {}
else if (eventType == "JunkStatusChanged") {}*/
}
},
_getCurrentFolderIndex: function () {
// for some reason, the index is interpreted as a string, therefore the parseInt
return parseInt(gFolderTreeView.getIndexOfFolder(gFolderTreeView.getSelectedFolders()[0]));
},
_getRSSUrl: function () {
return gDBView.hdrForFirstSelectedMessage.messageId.replace(/(#.*)?@.*$/, "");
},
_moveOrCopy: function (copy, destinationFolder, operateOnThread) {
let folders = mail.getFolders(destinationFolder);
if (folders.length == 0)
return void dactyl.echoerr("Exxx: No matching folder for " + destinationFolder);
else if (folders.length > 1)
return dactyl.echoerr("Exxx: More than one match for " + destinationFolder);
let count = gDBView.selection.count;
if (!count)
return void dactyl.beep();
(copy ? MsgCopyMessage : MsgMoveMessage)(folders[0]);
util.timeout(function () {
dactyl.echomsg(count + " message(s) " + (copy ? "copied" : "moved") + " to " + folders[0].prettyName, 1);
}, 100);
},
_parentIndex: function (index) {
let parent = index;
let tree = GetThreadTree();
while (true) {
let tmp = tree.view.getParentIndex(parent);
if (tmp >= 0)
parent = tmp;
else
break;
}
return parent;
},
// does not wrap yet, intentional?
_selectUnreadFolder: function (backwards, count) {
count = Math.max(1, count);
let direction = backwards ? -1 : 1;
let c = this._getCurrentFolderIndex();
let i = direction;
let folder;
while (count > 0 && (c + i) < gFolderTreeView.rowCount && (c + i) >= 0) {
let resource = gFolderTreeView._rowMap[c+i]._folder;
if (!resource.isServer && resource.getNumUnread(false)) {
count -= 1;
folder = i;
}
i += direction;
}
if (!folder || count > 0)
dactyl.beep();
else
gFolderTreeView.selection.timedSelect(c + folder, 500);
},
_escapeRecipient: function (recipient) {
// strip all ":
recipient = recipient.replace(/"/g, "");
return "\"" + recipient + "\"";
},
get currentAccount() this.currentFolder.rootFolder,
get currentFolder() gFolderTreeView.getSelectedFolders()[0],
/** @property {nsISmtpServer[]} The list of configured SMTP servers. */
get smtpServers() {
let servers = services.get("smtpService").smtpServers;
let ret = [];
while (servers.hasMoreElements()) {
let server = servers.getNext();
if (server instanceof Ci.nsISmtpServer)
ret.push(server);
}
return ret;
},
composeNewMail: function (args) {
let params = Cc["@mozilla.org/messengercompose/composeparams;1"].createInstance(Ci.nsIMsgComposeParams);
params.composeFields = Cc["@mozilla.org/messengercompose/composefields;1"].createInstance(Ci.nsIMsgCompFields);
if (args) {
if (args.originalMsg)
params.originalMsgURI = args.originalMsg;
if (args.to)
params.composeFields.to = args.to;
if (args.cc)
params.composeFields.cc = args.cc;
if (args.bcc)
params.composeFields.bcc = args.bcc;
if (args.newsgroups)
params.composeFields.newsgroups = args.newsgroups;
if (args.subject)
params.composeFields.subject = args.subject;
if (args.body)
params.composeFields.body = args.body;
if (args.attachments) {
while (args.attachments.length > 0) {
let url = args.attachments.pop();
let file = io.getFile(url);
if (!file.exists())
return void dactyl.echoerr("Exxx: Could not attach file `" + url + "'", commandline.FORCE_SINGLELINE);
attachment = Cc["@mozilla.org/messengercompose/attachment;1"].createInstance(Ci.nsIMsgAttachment);
attachment.url = "file://" + file.path;
params.composeFields.addAttachment(attachment);
}
}
}
params.type = Ci.nsIMsgCompType.New;
const msgComposeService = Cc["@mozilla.org/messengercompose;1"].getService();
msgComposeService = msgComposeService.QueryInterface(Ci.nsIMsgComposeService);
msgComposeService.OpenComposeWindowWithParams(null, params);
},
// returns an array of nsIMsgFolder objects
getFolders: function (filter, includeServers, includeMsgFolders) {
let folders = [];
if (!filter)
filter = "";
else
filter = filter.toLowerCase();
if (includeServers === undefined)
includeServers = false;
if (includeMsgFolders === undefined)
includeMsgFolders = true;
for (let i = 0; i < gFolderTreeView.rowCount; i++) {
let resource = gFolderTreeView._rowMap[i]._folder;
if ((resource.isServer && !includeServers) || (!resource.isServer && !includeMsgFolders))
continue;
let folderString = resource.server.prettyName + ": " + resource.name;
if (resource.prettiestName.toLowerCase().indexOf(filter) >= 0)
folders.push(resource);
else if (folderString.toLowerCase().indexOf(filter) >= 0)
folders.push(resource);
}
return folders;
},
getNewMessages: function (currentAccountOnly) {
if (currentAccountOnly)
MsgGetMessagesForAccount();
else
GetMessagesForAllAuthenticatedAccounts();
},
getStatistics: function (currentAccountOnly) {
let accounts = currentAccountOnly ? [this.currentAccount]
: this.getFolders("", true, false);
let unreadCount = 0, totalCount = 0, newCount = 0;
for (let i = 0; i < accounts.length; i++) {
let account = accounts[i];
unreadCount += account.getNumUnread(true); // true == deep (includes subfolders)
totalCount += account.getTotalMessages(true);
newCount += account.getNumUnread(true);
}
return { numUnread: unreadCount, numTotal: totalCount, numNew: newCount };
},
collapseThread: function () {
let tree = GetThreadTree();
if (tree) {
let parent = this._parentIndex(tree.currentIndex);
if (tree.changeOpenState(parent, false)) {
tree.view.selection.select(parent);
tree.treeBoxObject.ensureRowIsVisible(parent);
return true;
}
}
return false;
},
expandThread: function () {
let tree = GetThreadTree();
if (tree) {
let row = tree.currentIndex;
if (row >= 0 && tree.changeOpenState(row, true))
return true;
}
return false;
},
/**
* General-purpose method to find messages.
*
* @param {function(nsIMsgDBHdr):boolean} validatorFunc Return
* true/false whether msg should be selected or not.
* @param {boolean} canWrap When true, wraps around folders.
* @param {boolean} openThreads Should we open closed threads?
* @param {boolean} reverse Change direction of searching.
*/
selectMessage: function (validatorFunc, canWrap, openThreads, reverse, count) {
function currentIndex() {
let index = gDBView.selection.currentIndex;
if (index < 0)
index = 0;
return index;
}
function closedThread(index) {
if (!(gDBView.viewFlags & nsMsgViewFlagsType.kThreadedDisplay))
return false;
index = (typeof index == "number") ? index : currentIndex();
return !gDBView.isContainerOpen(index) && !gDBView.isContainerEmpty(index);
}
if (typeof validatorFunc != "function")
return;
if (typeof count != "number" || count < 1)
count = 1;
// first try to find in current folder
if (gDBView) {
for (let i = currentIndex() + (reverse ? -1 : (openThreads && closedThread() ? 0 : 1));
reverse ? (i >= 0) : (i < gDBView.rowCount);
reverse ? i-- : i++) {
let key = gDBView.getKeyAt(i);
let msg = gDBView.db.GetMsgHdrForKey(key);
// a closed thread
if (openThreads && closedThread(i)) {
let thread = gDBView.db.GetThreadContainingMsgHdr(msg);
let originalCount = count;
for (let j = (i == currentIndex() && !reverse) ? 1 : (reverse ? thread.numChildren - 1 : 0);
reverse ? (j >= 0) : (j < thread.numChildren);
reverse ? j-- : j++) {
msg = thread.getChildAt(j);
if (validatorFunc(msg) && --count == 0) {
// this hack is needed to get the correct message, because getChildAt() does not
// necessarily return the messages in the order they are displayed
gDBView.selection.timedSelect(i, GetThreadTree()._selectDelay || 500);
GetThreadTree().treeBoxObject.ensureRowIsVisible(i);
if (j > 0) {
GetThreadTree().changeOpenState(i, true);
this.selectMessage(validatorFunc, false, false, false, originalCount);
}
return;
}
}
}
else { // simple non-threaded message
if (validatorFunc(msg) && --count == 0) {
gDBView.selection.timedSelect(i, GetThreadTree()._selectDelay || 500);
GetThreadTree().treeBoxObject.ensureRowIsVisible(i);
return;
}
}
}
}
// then in other folders
if (canWrap) {
this._selectMessageReverse = reverse;
let folders = this.getFolders("", true, true);
let ci = this._getCurrentFolderIndex();
for (let i = 1; i < folders.length; i++) {
let index = (i + ci) % folders.length;
if (reverse)
index = folders.length - 1 - index;
let folder = folders[index];
if (folder.isServer)
continue;
this._selectMessageCount = count;
this._selectMessageKeys = [];
// sometimes folder.getMessages can fail with an exception
// TODO: find out why, and solve the problem
try {
var msgs = folder.messages;
}
catch (e) {
msgs = folder.getMessages(msgWindow); // for older thunderbirds
dactyl.dump("WARNING: " + folder.prettyName + " failed to getMessages, trying old API");
//continue;
}
while (msgs.hasMoreElements()) {
let msg = msgs.getNext().QueryInterface(Ci.nsIMsgDBHdr);
if (validatorFunc(msg)) {
count--;
this._selectMessageKeys.push(msg.messageKey);
}
}
if (count <= 0) {
// SelectFolder is asynchronous, message is selected in this._folderListener
SelectFolder(folder.URI);
return;
}
}
}
// TODO: finally for the "rest" of the current folder
dactyl.beep();
},
setHTML: function (value) {
let values = [[true, 1, gDisallow_classes_no_html], // plaintext
[false, 0, 0], // HTML
[false, 3, gDisallow_classes_no_html]]; // sanitized/simple HTML
if (typeof value != "number" || value < 0 || value > 2)
value = 1;
gPrefBranch.setBoolPref("mailnews.display.prefer_plaintext", values[value][0]);
gPrefBranch.setIntPref("mailnews.display.html_as", values[value][1]);
gPrefBranch.setIntPref("mailnews.display.disallow_mime_handlers", values[value][2]);
ReloadMessage();
}
}, {
}, {
commands: function () {
commands.add(["go[to]"],
"Select a folder",
function (args) {
let count = Math.max(0, args.count - 1);
let arg = args.literalArg || "Inbox";
let folder = mail.getFolders(arg, true, true)[count];
if (!folder)
dactyl.echoerr("Exxx: Folder \"" + arg + "\" does not exist");
else if (dactyl.forceNewTab)
MsgOpenNewTabForFolder(folder.URI);
else
SelectFolder(folder.URI);
},
{
argCount: "?",
completer: function (context) completion.mailFolder(context),
count: true,
literal: 0
});
commands.add(["m[ail]"],
"Write a new message",
function (args) {
let mailargs = {};
mailargs.to = args.join(", ");
mailargs.subject = args["-subject"];
mailargs.bcc = args["-bcc"];
mailargs.cc = args["-cc"];
mailargs.body = args["-text"];
mailargs.attachments = args["-attachment"] || [];
let addresses = args;
if (mailargs.bcc)
addresses = addresses.concat(mailargs.bcc);
if (mailargs.cc)
addresses = addresses.concat(mailargs.cc);
// TODO: is there a better way to check for validity?
if (addresses.some(function (recipient) !(/\S@\S+\.\S/.test(recipient))))
return void dactyl.echoerr("Exxx: Invalid e-mail address");
mail.composeNewMail(mailargs);
},
{
options: [[["-subject", "-s"], commands.OPTION_STRING],
[["-attachment", "-a"], commands.OPTION_LIST],
[["-bcc", "-b"], commands.OPTION_STRING],
[["-cc", "-c"], commands.OPTION_STRING],
[["-text", "-t"], commands.OPTION_STRING]]
});
commands.add(["copy[to]"],
"Copy selected messages",
function (args) { this._moveOrCopy(true, args.literalArg); },
{
argCount: 1,
completer: function (context) completion.mailFolder(context),
literal: 0
});
commands.add(["move[to]"],
"Move selected messages",
function (args) { this._moveOrCopy(false, args.literalArg); },
{
argCount: 1,
completer: function (context) completion.mailFolder(context),
literal: 0
});
commands.add(["empty[trash]"],
"Empty trash of the current account",
function () { window.goDoCommand("cmd_emptyTrash"); },
{ argCount: "0" });
commands.add(["get[messages]"],
"Check for new messages",
function (args) mail.getNewMessages(!args.bang),
{
argCount: "0",
bang: true,
});
},
completion: function () {
completion.mailFolder = function mailFolder(context) {
let folders = mail.getFolders(context.filter);
context.anchored = false;
context.quote = false;
context.completions = folders.map(function (folder)
[folder.server.prettyName + ": " + folder.name,
"Unread: " + folder.getNumUnread(false)]);
};
},
mappings: function () {
var myModes = config.mailModes;
mappings.add(myModes, ["<Return>", "i"],
"Inspect (focus) message",
function () { content.focus(); });
mappings.add(myModes, ["I"],
"Open the message in new tab",
function () {
if (gDBView && gDBView.selection.count < 1)
return void dactyl.beep();
MsgOpenNewTabForMessage();
});
/*mappings.add([modes.NORMAL],
["o"], "Open a message",
function () { commandline.open(":", "open ", modes.EX); });*/
mappings.add(myModes, ["<Space>"],
"Scroll message or select next unread one",
function () true,
{ route: true });
mappings.add(myModes, ["t"],
"Select thread",
function () { gDBView.ExpandAndSelectThreadByIndex(GetThreadTree().currentIndex, false); });
mappings.add(myModes, ["d", "<Del>"],
"Move mail to Trash folder",
function () { window.goDoCommand("cmd_delete"); });
mappings.add(myModes, ["j", "<Right>"],
"Select next message",
function (count) { mail.selectMessage(function (msg) true, false, false, false, count); },
{ count: true });
mappings.add(myModes, ["gj"],
"Select next message, including closed threads",
function (count) { mail.selectMessage(function (msg) true, false, true, false, count); },
{ count: true });
mappings.add(myModes, ["J", "<Tab>"],
"Select next unread message",
function (count) { mail.selectMessage(function (msg) !msg.isRead, true, true, false, count); },
{ count: true });
mappings.add(myModes, ["k", "<Left>"],
"Select previous message",
function (count) { mail.selectMessage(function (msg) true, false, false, true, count); },
{ count: true });
mappings.add(myModes, ["gk"],
"Select previous message",
function (count) { mail.selectMessage(function (msg) true, false, true, true, count); },
{ count: true });
mappings.add(myModes, ["K"],
"Select previous unread message",
function (count) { mail.selectMessage(function (msg) !msg.isRead, true, true, true, count); },
{ count: true });
mappings.add(myModes, ["*"],
"Select next message from the same sender",
function (count) {
try {
let author = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor.toLowerCase();
mail.selectMessage(function (msg) msg.mime2DecodedAuthor.toLowerCase().indexOf(author) == 0, true, true, false, count);
}
catch (e) { dactyl.beep(); }
},
{ count: true });
mappings.add(myModes, ["#"],
"Select previous message from the same sender",
function (count) {
try {
let author = gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor.toLowerCase();
mail.selectMessage(function (msg) msg.mime2DecodedAuthor.toLowerCase().indexOf(author) == 0, true, true, true, count);
}
catch (e) { dactyl.beep(); }
},
{ count: true });
// SENDING MESSAGES
mappings.add(myModes, ["m"],
"Compose a new message",
function () { commandline.open(":", "mail -subject=", modes.EX); });
mappings.add(myModes, ["M"],
"Compose a new message to the sender of selected mail",
function () {
try {
let to = this._escapeRecipient(gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor);
commandline.open(":", "mail " + to + " -subject=", modes.EX);
}
catch (e) {
dactyl.beep();
}
});
mappings.add(myModes, ["r"],
"Reply to sender",
function () { window.goDoCommand("cmd_reply"); });
mappings.add(myModes, ["R"],
"Reply to all",
function () { window.goDoCommand("cmd_replyall"); });
mappings.add(myModes, ["f"],
"Forward message",
function () { window.goDoCommand("cmd_forward"); });
mappings.add(myModes, ["F"],
"Forward message inline",
function () { window.goDoCommand("cmd_forwardInline"); });
// SCROLLING
mappings.add(myModes, ["<Down>"],
"Scroll message down",
function (count) { buffer.scrollLines(Math.max(count, 1)); },
{ count: true });
mappings.add(myModes, ["<Up>"],
"Scroll message up",
function (count) { buffer.scrollLines(-Math.max(count, 1)); },
{ count: true });
mappings.add([modes.MESSAGE], ["<Left>"],
"Select previous message",
function (count) { mail.selectMessage(function (msg) true, false, false, true, count); },
{ count: true });
mappings.add([modes.MESSAGE], ["<Right>"],
"Select next message",
function (count) { mail.selectMessage(function (msg) true, false, false, false, count); },
{ count: true });
// UNDO/REDO
mappings.add(myModes, ["u"],
"Undo",
function () {
if (messenger.canUndo())
messenger.undo(msgWindow);
else
dactyl.beep();
});
mappings.add(myModes, ["<C-r>"],
"Redo",
function () {
if (messenger.canRedo())
messenger.redo(msgWindow);
else
dactyl.beep();
});
// GETTING MAIL
mappings.add(myModes, ["gm"],
"Get new messages",
function () { mail.getNewMessages(); });
mappings.add(myModes, ["gM"],
"Get new messages for current account only",
function () { mail.getNewMessages(true); });
// MOVING MAIL
mappings.add(myModes, ["c"],
"Change folders",
function () { commandline.open(":", "goto ", modes.EX); });
mappings.add(myModes, ["s"],
"Move selected messages",
function () { commandline.open(":", "moveto ", modes.EX); });
mappings.add(myModes, ["S"],
"Copy selected messages",
function () { commandline.open(":", "copyto ", modes.EX); });
mappings.add(myModes, ["<C-s>"],
"Archive message",
function () { this._moveOrCopy(false, options["archivefolder"]); });
mappings.add(myModes, ["]s"],
"Select next starred message",
function (count) { mail.selectMessage(function (msg) msg.isFlagged, true, true, false, count); },
{ count: true });
mappings.add(myModes, ["[s"],
"Select previous starred message",
function (count) { mail.selectMessage(function (msg) msg.isFlagged, true, true, true, count); },
{ count: true });
mappings.add(myModes, ["]a"],
"Select next message with an attachment",
function (count) { mail.selectMessage(function (msg) gDBView.db.HasAttachments(msg.messageKey), true, true, false, count); },
{ count: true });
mappings.add(myModes, ["[a"],
"Select previous message with an attachment",
function (count) { mail.selectMessage(function (msg) gDBView.db.HasAttachments(msg.messageKey), true, true, true, count); },
{ count: true });
// FOLDER SWITCHING
mappings.add(myModes, ["gi"],
"Go to inbox",
function (count) {
let folder = mail.getFolders("Inbox", false, true)[(count > 0) ? (count - 1) : 0];
if (folder)
SelectFolder(folder.URI);
else
dactyl.beep();
},
{ count: true });
mappings.add(myModes, ["<C-n>"],
"Select next folder",
function (count) {
count = Math.max(1, count);
let newPos = this._getCurrentFolderIndex() + count;
if (newPos >= gFolderTreeView.rowCount) {
newPos = newPos % gFolderTreeView.rowCount;
commandline.echo("search hit BOTTOM, continuing at TOP", commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES);
}
gFolderTreeView.selection.timedSelect(newPos, 500);
},
{ count: true });
mappings.add(myModes, ["<C-N>"],
"Go to next mailbox with unread messages",
function (count) {
this._selectUnreadFolder(false, count);
},
{ count: true });
mappings.add(myModes, ["<C-p>"],
"Select previous folder",
function (count) {
count = Math.max(1, count);
let newPos = this._getCurrentFolderIndex() - count;
if (newPos < 0) {
newPos = (newPos % gFolderTreeView.rowCount) + gFolderTreeView.rowCount;
commandline.echo("search hit TOP, continuing at BOTTOM", commandline.HL_WARNINGMSG, commandline.APPEND_TO_MESSAGES);
}
gFolderTreeView.selection.timedSelect(newPos, 500);
},
{ count: true });
mappings.add(myModes, ["<C-P>"],
"Go to previous mailbox with unread messages",
function (count) {
this._selectUnreadFolder(true, count);
},
{ count: true });
// THREADING
mappings.add(myModes, ["za"],
"Toggle thread collapsed/expanded",
function () { if (!mail.expandThread()) mail.collapseThread(); });
mappings.add(myModes, ["zc"],
"Collapse thread",
function () { mail.collapseThread(); });
mappings.add(myModes, ["zo"],
"Open thread",
function () { mail.expandThread(); });
mappings.add(myModes, ["zr", "zR"],
"Expand all threads",
function () { window.goDoCommand("cmd_expandAllThreads"); });
mappings.add(myModes, ["zm", "zM"],
"Collapse all threads",
function () { window.goDoCommand("cmd_collapseAllThreads"); });
mappings.add(myModes, ["<C-i>"],
"Go forward",
function (count) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.forward, true); },
{ count: true });
mappings.add(myModes, ["<C-o>"],
"Go back",
function (count) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.back, true); },
{ count: true });
mappings.add(myModes, ["gg"],
"Select first message",
function (count) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.firstMessage, true); },
{ count: true });
mappings.add(myModes, ["G"],
"Select last message",
function (count) { if (count < 1) count = 1; while (count--) GoNextMessage(nsMsgNavigationType.lastMessage, false); },
{ count: true });
// tagging messages
mappings.add(myModes, ["l"],
"Label message",
function (arg) {
if (!GetSelectedMessages())
return void dactyl.beep();
switch (arg) {
case "r": MsgMarkMsgAsRead(); break;
case "s": MsgMarkAsFlagged(); break;
case "i": ToggleMessageTagKey(1); break; // Important
case "w": ToggleMessageTagKey(2); break; // Work
case "p": ToggleMessageTagKey(3); break; // Personal
case "t": ToggleMessageTagKey(4); break; // TODO
case "l": ToggleMessageTagKey(5); break; // Later
default: dactyl.beep();
}
},
{
arg: true
});
// TODO: change binding?
mappings.add(myModes, ["T"],
"Mark current folder as read",
function () {
if (mail.currentFolder.isServer)
return dactyl.beep();
mail.currentFolder.markAllMessagesRead(msgWindow);
});
mappings.add(myModes, ["<C-t>"],
"Mark all messages as read",
function () {
mail.getFolders("", false).forEach(function (folder) { folder.markAllMessagesRead(msgWindow); });
});
// DISPLAY OPTIONS
mappings.add(myModes, ["h"],
"Toggle displayed headers",
function () {
let value = gPrefBranch.getIntPref("mail.show_headers", 2);
gPrefBranch.setIntPref("mail.show_headers", value == 2 ? 1 : 2);
ReloadMessage();
});
mappings.add(myModes, ["x"],
"Toggle HTML message display",
function () {
let wantHtml = (gPrefBranch.getIntPref("mailnews.display.html_as", 1) == 1);
mail.setHTML(wantHtml ? 1 : 0);
});
// YANKING TEXT
mappings.add(myModes, ["Y"],
"Yank subject",
function () {
try {
let subject = gDBView.hdrForFirstSelectedMessage.mime2DecodedSubject;
util.copyToClipboard(subject, true);
}
catch (e) { dactyl.beep(); }
});
mappings.add(myModes, ["y"],
"Yank sender or feed URL",
function () {
try {
if (mail.currentAccount.server.type == "rss")
util.copyToClipboard(this._getRSSUrl(), true);
else
util.copyToClipboard(gDBView.hdrForFirstSelectedMessage.mime2DecodedAuthor, true);
}
catch (e) { dactyl.beep(); }
});
// RSS specific mappings
mappings.add(myModes, ["p"],
"Open RSS message in browser",
function () {
try {
if (mail.currentAccount.server.type == "rss")
messenger.launchExternalURL(this._getRSSUrl());
// TODO: what to do for non-rss message?
}
catch (e) {
dactyl.beep();
}
});
},
options: function () {
// FIXME: why does this default to "Archive", I don't have one? The default
// value won't validate now. mst please fix. --djk
options.add(["archivefolder"],
"Set the archive folder",
"string", "Archive",
{
completer: function (context) completion.mailFolder(context),
validator: Option.validateCompleter
});
// TODO: generate the possible values dynamically from the menu
options.add(["layout"],
"Set the layout of the mail window",
"string", "inherit",
{
setter: function (value) {
switch (value) {
case "classic": ChangeMailLayout(0); break;
case "wide": ChangeMailLayout(1); break;
case "vertical": ChangeMailLayout(2); break;
// case "inherit" just does nothing
}
return value;
},
completer: function (context) [
["inherit", "Default View"], // FIXME: correct description?
["classic", "Classic View"],
["wide", "Wide View"],
["vertical", "Vertical View"]
],
validator: Option.validateCompleter
});
options.add(["smtpserver", "smtp"],
"Set the default SMTP server",
"string", services.get("smtpService").defaultServer.key, // TODO: how should we handle these persistent external defaults - "inherit" or null?
{
getter: function () services.get("smtpService").defaultServer.key,
setter: function (value) {
let server = mail.smtpServers.filter(function (s) s.key == value)[0];
services.get("smtpService").defaultServer = server;
return value;
},
completer: function (context) [[s.key, s.serverURI] for ([, s] in Iterator(mail.smtpServers))],
validator: Option.validateCompleter
});
/*options.add(["threads"],
"Use threading to group messages",
"boolean", true,
{
setter: function (value) {
if (value)
MsgSortThreaded();
else
MsgSortUnthreaded();
return value;
}
});*/
}
});
// vim: set fdm=marker sw=4 ts=4 et:

View File

@@ -0,0 +1,20 @@
<?xml version="1.0"?>
<!-- ***** BEGIN LICENSE BLOCK ***** {{{
Copyright (c) 2006-2009 by Martin Stubenschrott <stubenschrott@vimperator.org>
This work is licensed for reuse under an MIT license. Details are
given in the LICENSE.txt file included with this file.
}}} ***** END LICENSE BLOCK ***** -->
<!--?xml-stylesheet href="chrome://browser/skin/" type="text/css"?-->
<overlay id="teledactyl"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:nc="http://home.netscape.com/NC-rdf#"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
</overlay>
<!-- vim: set fdm=marker sw=4 ts=4 et: -->