diff --git a/Convert.alfredworkflow b/Convert.alfredworkflow index 8d31429..799d85f 100644 Binary files a/Convert.alfredworkflow and b/Convert.alfredworkflow differ diff --git a/README.md b/README.md index 2eb69d5..e65e0da 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,25 @@ -title: Alfred-Convert Help # Alfred-Convert # -Convert between different units in [Alfred 2](http://www.alfredapp.com/). +Convert between different units in [Alfred 2][alfred]. -![](https://raw.github.com/deanishe/alfred-convert/master/screenshot1.png "") +![][demo] Alfred-Convert uses a [built-in library](http://pint.readthedocs.org/en/latest/index.html) to do the conversion offline. -**Note:** Currency conversions do require occasional Internet connectivity to update exchange rates. **Alfred-Convert** will otherwise work just fine without an Internet connection. +**Note:** Currency conversions do require occasional Internet connectivity to update exchange rates. Alfred-Convert will otherwise work just fine without an Internet connection. + +- [Downloading](#downloading) +- [Usage](#usage) +- [Supported units](#supported-units) + - [Supported currencies](#supported-currencies) + - [Adding custom units](#adding-custom-units) +- [Thanks, copyright, licensing](#thanks-copyright-licensing) + ## Downloading ## -Download from [GitHub](https://github.com/deanishe/alfred-convert/blob/master/Convert.alfredworkflow?raw=true). +Download from [GitHub][ghreleases] or [Packal.org][packal]. ## Usage ## @@ -23,25 +30,27 @@ The syntax is simple: the quantity, the unit you want to convert from then the u - `conv 100psi bar` - `conv 20.5 m/s mph` -It doesn't matter if there is a space between the quantity and the units or not. **Alfred-Convert** will tell you if it doesn't understand your query or know the units. - -Actioning an item (selecting it and hitting `ENTER`) will copy it to the clipboard. +It doesn't matter if there is a space between the quantity and the units or not. Alfred-Convert will tell you if it doesn't understand your query or know the units. -Use `convhelp` to view the built-in help file (this file) and `convcurrencies` to view and search the list of supported currencies. +Actioning an item (selecting it and hitting `↩`) will copy it to the clipboard. +Using `⌘+L` will display the result in Alfred's large text window, `⌘+C` will +copy the selected result to the clipboard. -## Screenshots ## +Use `convinfo` to view the built-in help file, view/search the list of +supported currencies, change the number of decimal places shown in conversions, +or edit your custom units. -![](https://raw.github.com/deanishe/alfred-convert/master/screenshot1.png "") - -![](https://raw.github.com/deanishe/alfred-convert/master/screenshot2.png "") - -![](https://raw.github.com/deanishe/alfred-convert/master/screenshot3.png "") ## Supported units ## -Currently, **Alfred-Convert** only supports [the units](https://raw.github.com/deanishe/alfred-convert/master/src/pint/default_en.txt) understood by the underlying [Pint](http://pint.readthedocs.org/en/latest/index.html) library (plus currencies). +Currently, Alfred-Convert only supports [the units][pintunits] understood +by the underlying [Pint][pintdocs] library plus +[currencies](#supported-currencies) and a handful of additional units. + +You can [your own custom units](#adding-custom-units) to the workflow. If youthink they'd be useful to everyone, please create a corresponding +[GitHub issue][ghissues] to request addition as a default unit or submit a +[pull request][ghpulls]. -If you'd like to see more units added, please create a corresponding [GitHub issue](https://github.com/deanishe/alfred-convert/issues). ### Supported currencies ### @@ -49,48 +58,200 @@ To convert, use the appropriate **abbreviation** for the relevant currencies. You can also view (and search) the list from within Alfred using the keyword `convcurrencies`. -| Name | Abbreviation | -|-----------------------|--------------| -| Australian dollar | AUD | -| Bulgarian lev | BGN | -| Brasilian real | BRL | -| Canadian dollar | CAD | -| Swiss franc | CHF | -| Chinese yuan renminbi | CNY | -| Czech koruna | CZK | -| Danish krone | DKK | -| Euro | EUR | -| Pound sterling | GBP | -| Hong Kong dollar | HKD | -| Croatian kuna | HRK | -| Hungarian forint | HUF | -| Indonesian rupiah | IDR | -| Israeli shekel | ILS | -| Indian rupee | INR | -| Japanese yen | JPY | -| South Korean won | KRW | -| Lithuanian litas | LTL | -| Mexican peso | MXN | -| Malaysian ringgit | MYR | -| Norwegian krone | NOK | -| New Zealand dollar | NZD | -| Philippine peso | PHP | -| Polish zloty | PLN | -| New Romanian leu | RON | -| Russian rouble | RUB | -| Swedish krona | SEK | -| Singapore dollar | SGD | -| Thai baht | THB | -| Turkish lira | TRY | -| US dollar | USD | -| South African rand | ZAR | +| Abbreviation | Name | +|--------------|----------------------------------------------------------| +| AED | United Arab Emirates Dirham | +| AFN | Afghanistan Afghani | +| ALL | Albania Lek | +| AMD | Armenia Dram | +| ANG | Netherlands Antilles Guilder | +| AOA | Angola Kwanza | +| ARS | Argentina Peso | +| AUD | Australia Dollar | +| AWG | Aruba Guilder | +| AZN | Azerbaijan New Manat | +| BAM | Bosnia and Herzegovina Convertible Marka | +| BBD | Barbados Dollar | +| BDT | Bangladesh Taka | +| BGN | Bulgaria Lev | +| BHD | Bahrain Dinar | +| BIF | Burundi Franc | +| BMD | Bermuda Dollar | +| BND | Brunei Darussalam Dollar | +| BOB | Bolivia Boliviano | +| BRL | Brazil Real | +| BSD | Bahamas Dollar | +| BTN | Bhutan Ngultrum | +| BWP | Botswana Pula | +| BYR | Belarus Ruble | +| BZD | Belize Dollar | +| CAD | Canada Dollar | +| CDF | Congo/Kinshasa Franc | +| CHF | Switzerland Franc | +| CLP | Chile Peso | +| CNY | China Yuan Renminbi | +| COP | Colombia Peso | +| CRC | Costa Rica Colon | +| CUP | Cuba Peso | +| CVE | Cape Verde Escudo | +| CZK | Czech Republic Koruna | +| DJF | Djibouti Franc | +| DKK | Denmark Krone | +| DOP | Dominican Republic Peso | +| DZD | Algeria Dinar | +| EGP | Egypt Pound | +| ERN | Eritrea Nakfa | +| ETB | Ethiopia Birr | +| EUR | Euro Member Countries | +| FJD | Fiji Dollar | +| FKP | Falkland Islands (Malvinas) Pound | +| GBP | United Kingdom Pound | +| GEL | Georgia Lari | +| GHS | Ghana Cedi | +| GIP | Gibraltar Pound | +| GMD | Gambia Dalasi | +| GNF | Guinea Franc | +| GTQ | Guatemala Quetzal | +| GYD | Guyana Dollar | +| HKD | Hong Kong Dollar | +| HNL | Honduras Lempira | +| HRK | Croatia Kuna | +| HTG | Haiti Gourde | +| HUF | Hungary Forint | +| IDR | Indonesia Rupiah | +| ILS | Israel Shekel | +| INR | India Rupee | +| IQD | Iraq Dinar | +| IRR | Iran Rial | +| ISK | Iceland Krona | +| JMD | Jamaica Dollar | +| JOD | Jordan Dinar | +| JPY | Japan Yen | +| KES | Kenya Shilling | +| KGS | Kyrgyzstan Som | +| KHR | Cambodia Riel | +| KMF | Comoros Franc | +| KPW | Korea (North) Won | +| KRW | Korea (South) Won | +| KWD | Kuwait Dinar | +| KYD | Cayman Islands Dollar | +| KZT | Kazakhstan Tenge | +| LAK | Laos Kip | +| LBP | Lebanon Pound | +| LKR | Sri Lanka Rupee | +| LRD | Liberia Dollar | +| LSL | Lesotho Loti | +| LTL | Lithuania Litas | +| LYD | Libya Dinar | +| MAD | Morocco Dirham | +| MDL | Moldova Leu | +| MGA | Madagascar Ariary | +| MKD | Macedonia Denar | +| MMK | Myanmar (Burma) Kyat | +| MNT | Mongolia Tughrik | +| MOP | Macau Pataca | +| MRO | Mauritania Ouguiya | +| MUR | Mauritius Rupee | +| MVR | Maldives (Maldive Islands) Rufiyaa | +| MWK | Malawi Kwacha | +| MXN | Mexico Peso | +| MYR | Malaysia Ringgit | +| MZN | Mozambique Metical | +| NAD | Namibia Dollar | +| NGN | Nigeria Naira | +| NIO | Nicaragua Cordoba | +| NOK | Norway Krone | +| NPR | Nepal Rupee | +| NZD | New Zealand Dollar | +| OMR | Oman Rial | +| PAB | Panama Balboa | +| PEN | Peru Nuevo Sol | +| PGK | Papua New Guinea Kina | +| PHP | Philippines Peso | +| PKR | Pakistan Rupee | +| PLN | Poland Zloty | +| PYG | Paraguay Guarani | +| QAR | Qatar Riyal | +| RON | Romania New Leu | +| RSD | Serbia Dinar | +| RUB | Russia Ruble | +| RWF | Rwanda Franc | +| SAR | Saudi Arabia Riyal | +| SBD | Solomon Islands Dollar | +| SCR | Seychelles Rupee | +| SDG | Sudan Pound | +| SEK | Sweden Krona | +| SGD | Singapore Dollar | +| SHP | Saint Helena Pound | +| SLL | Sierra Leone Leone | +| SOS | Somalia Shilling | +| SRD | Suriname Dollar | +| STD | São Tomé and Príncipe Dobra | +| SVC | El Salvador Colon | +| SYP | Syria Pound | +| SZL | Swaziland Lilangeni | +| THB | Thailand Baht | +| TJS | Tajikistan Somoni | +| TMT | Turkmenistan Manat | +| TND | Tunisia Dinar | +| TOP | Tonga Pa'anga | +| TRY | Turkey Lira | +| TTD | Trinidad and Tobago Dollar | +| TWD | Taiwan New Dollar | +| TZS | Tanzania Shilling | +| UAH | Ukraine Hryvnia | +| UGX | Uganda Shilling | +| USD | United States Dollar | +| UYU | Uruguay Peso | +| UZS | Uzbekistan Som | +| VEF | Venezuela Bolivar | +| VND | Viet Nam Dong | +| VUV | Vanuatu Vatu | +| WST | Samoa Tala | +| XAF | Communauté Financière Africaine (BEAC) CFA Franc BEAC | +| XCD | East Caribbean Dollar | +| XDR | International Monetary Fund (IMF) Special Drawing Rights | +| XOF | Communauté Financière Africaine (BCEAO) Franc | +| XPF | Comptoirs Français du Pacifique (CFP) Franc | +| YER | Yemen Rial | +| ZAR | South Africa Rand | +| ZMW | Zambia Kwacha | -## Thanks, copyright, licensing ## +### Adding custom units ### + +You can add your own custom units using the +[format defined by Pint][pinthowto]. Add your definitions to the +`unit_definitions.txt` file in the workflow's data directory. -The Python [Pint](http://pint.readthedocs.org/en/latest/index.html) library does all the heavy lifting. See the [Pint GitHub repo](https://github.com/hgrecco/pint) for Pint licensing or `LICENSE.txt` and `AUTHORS.txt` in the `pint` subdirectory. +To edit this file, enter `convinfo` in Alfred and select `Edit Custom Units`. +The `unit_definitions.txt` file will open in your default text editor. -The money icon is from [Gettyicons.com](http://www.gettyicons.com/free-icon/105/money-icon-set/free-money-icon-png/) and was created by [DaPino](http://www.dapino-colada.nl/). +Please see the [Pint documentation][pinthowto] for the required format. See +Pint's [default unit definitions][pintunits] for examples. -Exchange rates are downloaded from the [European Central Bank's website](http://www.ecb.europa.eu/stats/exchange/eurofxref/html/index.en.html). +## Thanks, copyright, licensing ## + +- The Python [Pint][pintdocs] library does all the heavy lifting. See the [Pint GitHub repo][pintrepo] for Pint licensing or `LICENSE.txt` and `AUTHORS.txt` in the `pint` subdirectory. +- The money icon is from [Gettyicons.com][getty] and was created by +[DaPino][dapino]. +- The workflow icon is from [Font Awesome][fontawesome] +- Exchange rates are downloaded from the [Yahoo! Finance][yahoo-finance]. +- The [Alfred-Workflow][alfred-workflow] library is used heavily. All other code/media are released under the [MIT Licence](http://opensource.org/licenses/MIT). + +[pintdocs]: http://pint.readthedocs.org/en/latest/index.html +[pintrepo]: https://github.com/hgrecco/pint +[pinthowto]: http://pint.readthedocs.org/en/latest/defining.html +[pintunits]: https://github.com/hgrecco/pint/blob/master/pint/default_en.txt +[ghissues]: https://github.com/deanishe/alfred-convert/issues +[ghpulls]: https://github.com/deanishe/alfred-convert/pulls +[ghreleases]: https://github.com/deanishe/alfred-convert/releases +[packal]: http://www.packal.org/workflow/convert +[yahoo-finance]: http://finance.yahoo.com/ +[getty]: http://www.gettyicons.com/free-icon/105/money-icon-set/free-money-icon-png/ +[dapino]: http://www.dapino-colada.nl/ +[fontawesome]: http://fortawesome.github.io/Font-Awesome/ +[alfred-workflow]: http://www.deanishe.net/alfred-workflow/ +[alfred]: http://www.alfredapp.com/ +[demo]: https://raw.github.com/deanishe/alfred-convert/master/demo.gif diff --git a/demo.gif b/demo.gif new file mode 100644 index 0000000..acc7509 Binary files /dev/null and b/demo.gif differ diff --git a/Icon.png b/extra/Icon.png similarity index 100% rename from Icon.png rename to extra/Icon.png diff --git a/screenshot1.png b/screenshot1.png deleted file mode 100644 index f93f8c1..0000000 Binary files a/screenshot1.png and /dev/null differ diff --git a/screenshot2.png b/screenshot2.png deleted file mode 100644 index 458fe0b..0000000 Binary files a/screenshot2.png and /dev/null differ diff --git a/screenshot3.png b/screenshot3.png deleted file mode 100644 index 55cc303..0000000 Binary files a/screenshot3.png and /dev/null differ diff --git a/src/README.html b/src/README.html index 98e5de3..721417c 100644 --- a/src/README.html +++ b/src/README.html @@ -6,651 +6,598 @@ Alfred-Convert Help @@ -660,17 +607,29 @@

Alfred-Convert

Convert between different units in Alfred 2.

-
- -
+

Alfred-Convert uses a built-in library to do the conversion offline.

-

Note: Currency conversions do require occasional Internet connectivity to update exchange rates. Alfred-Convert will otherwise work just fine without an Internet connection.

+

Note: Currency conversions do require occasional Internet connectivity to update exchange rates. Alfred-Convert will otherwise work just fine without an Internet connection.

+ + +

Downloading

-

Download from GitHub.

+

Download from GitHub or Packal.org.

Usage

@@ -683,196 +642,4969 @@

Usage

  • conv 20.5 m/s mph
  • -

    It doesn’t matter if there is a space between the quantity and the units or not. Alfred-Convert will tell you if it doesn’t understand your query or know the units.

    - -

    Actioning an item (selecting it and hitting ENTER) will copy it to the clipboard.

    - -

    Use convhelp to view the built-in help file (this file) and convcurrencies to view and search the list of supported currencies.

    - -

    Screenshots

    -
    - -
    +

    It doesn’t matter if there is a space between the quantity and the units or not. Alfred-Convert will tell you if it doesn’t understand your query or know the units.

    -
    - -
    +

    Actioning an item (selecting it and hitting ) will copy it to the clipboard. +Using ⌘+L will display the result in Alfred’s large text window, ⌘+C will +copy the selected result to the clipboard.

    -
    - -
    +

    Use convinfo to view the built-in help file, view/search the list of +supported currencies, change the number of decimal places shown in conversions, +or edit your custom units.

    -

    Supported units

    +

    Supported units

    -

    Currently, Alfred-Convert only supports the units understood by the underlying Pint library (plus currencies).

    +

    Currently, Alfred-Convert only supports the units understood +by the underlying Pint library plus +currencies and a handful of additional units.

    -

    If you’d like to see more units added, please create a corresponding GitHub issue.

    +

    You can your own custom units to the workflow. If youthink they’d be useful to everyone, please create a corresponding +GitHub issue to request addition as a default unit or submit a +pull request.

    -

    Supported currencies

    +

    Supported currencies

    To convert, use the appropriate abbreviation for the relevant currencies.

    You can also view (and search) the list from within Alfred using the keyword convcurrencies.

    ---- - - - + + - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    NameAbbreviation Abbreviation Name
    Australian dollarAUD AED United Arab Emirates Dirham
    AFN Afghanistan Afghani
    ALL Albania Lek
    AMD Armenia Dram
    ANG Netherlands Antilles Guilder
    AOA Angola Kwanza
    ARS Argentina Peso
    AUD Australia Dollar
    AWG Aruba Guilder
    AZN Azerbaijan New Manat
    BAM Bosnia and Herzegovina Convertible Marka
    BBD Barbados Dollar
    BDT Bangladesh Taka
    BGN Bulgaria Lev
    BHD Bahrain Dinar
    BIF Burundi Franc
    BMD Bermuda Dollar
    BND Brunei Darussalam Dollar
    BOB Bolivia Boliviano
    BRL Brazil Real
    BSD Bahamas Dollar
    BTN Bhutan Ngultrum
    BWP Botswana Pula
    BYR Belarus Ruble
    BZD Belize Dollar
    CAD Canada Dollar
    CDF Congo/Kinshasa Franc
    CHF Switzerland Franc
    CLP Chile Peso
    CNY China Yuan Renminbi
    COP Colombia Peso
    CRC Costa Rica Colon
    CUP Cuba Peso
    CVE Cape Verde Escudo
    CZK Czech Republic Koruna
    DJF Djibouti Franc
    DKK Denmark Krone
    DOP Dominican Republic Peso
    DZD Algeria Dinar
    EGP Egypt Pound
    ERN Eritrea Nakfa
    ETB Ethiopia Birr
    EUR Euro Member Countries
    FJD Fiji Dollar
    FKP Falkland Islands (Malvinas) Pound
    GBP United Kingdom Pound
    GEL Georgia Lari
    GHS Ghana Cedi
    GIP Gibraltar Pound
    GMD Gambia Dalasi
    GNF Guinea Franc
    GTQ Guatemala Quetzal
    GYD Guyana Dollar
    HKD Hong Kong Dollar
    HNL Honduras Lempira
    HRK Croatia Kuna
    HTG Haiti Gourde
    HUF Hungary Forint
    IDR Indonesia Rupiah
    ILS Israel Shekel
    INR India Rupee
    IQD Iraq Dinar
    IRR Iran Rial
    ISK Iceland Krona
    JMD Jamaica Dollar
    JOD Jordan Dinar
    JPY Japan Yen
    KES Kenya Shilling
    KGS Kyrgyzstan Som
    KHR Cambodia Riel
    KMF Comoros Franc
    KPW Korea (North) Won
    KRW Korea (South) Won
    KWD Kuwait Dinar
    KYD Cayman Islands Dollar
    KZT Kazakhstan Tenge
    LAK Laos Kip
    Bulgarian levBGN LBP Lebanon Pound
    Brasilian realBRL LKR Sri Lanka Rupee
    Canadian dollarCAD LRD Liberia Dollar
    Swiss francCHF LSL Lesotho Loti
    Chinese yuan renminbiCNY LTL Lithuania Litas
    Czech korunaCZK LYD Libya Dinar
    Danish kroneDKK MAD Morocco Dirham
    EuroEUR MDL Moldova Leu
    Pound sterlingGBP MGA Madagascar Ariary
    Hong Kong dollarHKD MKD Macedonia Denar
    Croatian kunaHRK MMK Myanmar (Burma) Kyat
    Hungarian forintHUF MNT Mongolia Tughrik
    Indonesian rupiahIDR MOP Macau Pataca
    Israeli shekelILS MRO Mauritania Ouguiya
    Indian rupeeINR MUR Mauritius Rupee
    Japanese yenJPY MVR Maldives (Maldive Islands) Rufiyaa
    South Korean wonKRW MWK Malawi Kwacha
    Lithuanian litasLTL MXN Mexico Peso
    Mexican pesoMXN MYR Malaysia Ringgit
    Malaysian ringgitMYR MZN Mozambique Metical
    Norwegian kroneNOK NAD Namibia Dollar
    New Zealand dollarNZD NGN Nigeria Naira
    Philippine pesoPHP NIO Nicaragua Cordoba
    Polish zlotyPLN NOK Norway Krone
    New Romanian leuRON NPR Nepal Rupee
    Russian roubleRUB NZD New Zealand Dollar
    Swedish kronaSEK OMR Oman Rial
    Singapore dollarSGD PAB Panama Balboa
    Thai bahtTHB PEN Peru Nuevo Sol
    Turkish liraTRY PGK Papua New Guinea Kina
    US dollarUSD PHP Philippines Peso
    South African randZAR PKR Pakistan Rupee
    PLN Poland Zloty
    PYG Paraguay Guarani
    QAR Qatar Riyal
    RON Romania New Leu
    RSD Serbia Dinar
    RUB Russia Ruble
    RWF Rwanda Franc
    SAR Saudi Arabia Riyal
    SBD Solomon Islands Dollar
    SCR Seychelles Rupee
    SDG Sudan Pound
    SEK Sweden Krona
    SGD Singapore Dollar
    SHP Saint Helena Pound
    SLL Sierra Leone Leone
    SOS Somalia Shilling
    SRD Suriname Dollar
    STD São Tomé and Príncipe Dobra
    SVC El Salvador Colon
    SYP Syria Pound
    SZL Swaziland Lilangeni
    THB Thailand Baht
    TJS Tajikistan Somoni
    TMT Turkmenistan Manat
    TND Tunisia Dinar
    TOP Tonga Pa'anga
    TRY Turkey Lira
    TTD Trinidad and Tobago Dollar
    TWD Taiwan New Dollar
    TZS Tanzania Shilling
    UAH Ukraine Hryvnia
    UGX Uganda Shilling
    USD United States Dollar
    UYU Uruguay Peso
    UZS Uzbekistan Som
    VEF Venezuela Bolivar
    VND Viet Nam Dong
    VUV Vanuatu Vatu
    WST Samoa Tala
    XAF Communauté Financière Africaine (BEAC) CFA Franc BEAC
    XCD East Caribbean Dollar
    XDR International Monetary Fund (IMF) Special Drawing Rights
    XOF Communauté Financière Africaine (BCEAO) Franc
    XPF Comptoirs Français du Pacifique (CFP) Franc
    YER Yemen Rial
    ZAR South Africa Rand
    ZMW Zambia Kwacha
    -

    Thanks, copyright, licensing

    -

    The Python Pint library does all the heavy lifting. See the Pint GitHub repo for Pint licensing or LICENSE.txt and AUTHORS.txt in the pint subdirectory.

    +

    Adding custom units

    + +

    You can add your own custom units using the +format defined by Pint. Add your definitions to the +unit_definitions.txt file in the workflow’s data directory.

    + +

    To edit this file, enter convinfo in Alfred and select Edit Custom Units. +The unit_definitions.txt file will open in your default text editor.

    -

    The money icon is from Gettyicons.com and was created by DaPino.

    +

    Please see the Pint documentation for the required format. See +Pint’s default unit definitions for examples.

    + + + + -

    Exchange rates are downloaded from the European Central Bank’s website.

    All other code/media are released under the MIT Licence.

    + +
    + + + +
    + diff --git a/src/config.py b/src/config.py index b29020a..9be62e8 100644 --- a/src/config.py +++ b/src/config.py @@ -20,6 +20,13 @@ CURRENCY_CACHE_AGE = 3600 * 12 # 12 hours CURRENCY_CACHE_NAME = 'exchange_rates' +DECIMAL_PLACES_DEFAULT = 2 + +CUSTOM_DEFINITIONS_FILENAME = 'unit_definitions.txt' + +BUILTIN_UNIT_DEFINITIONS = os.path.join(os.path.dirname(__file__), + CUSTOM_DEFINITIONS_FILENAME) + with open(os.path.join(os.path.dirname(__file__), 'currencies.json'), 'rb') as fp: CURRENCIES = json.load(fp) @@ -36,4 +43,4 @@ UPDATE_SETTINGS = {'github_slug': 'deanishe/alfred-convert'} -DEFAULT_SETTINGS = {} +DEFAULT_SETTINGS = {'decimal_places': DECIMAL_PLACES_DEFAULT} diff --git a/src/convert.py b/src/convert.py index b54c796..4448781 100755 --- a/src/convert.py +++ b/src/convert.py @@ -13,14 +13,19 @@ from __future__ import print_function, unicode_literals +import os +import shutil import sys from pint import UnitRegistry, UndefinedUnitError from workflow import Workflow, ICON_WARNING, ICON_INFO from workflow.background import run_in_background, is_running -from config import (CURRENCY_CACHE_AGE, CURRENCY_CACHE_NAME, ICON_UPDATE, - UPDATE_SETTINGS, DEFAULT_SETTINGS) +from config import (CURRENCY_CACHE_AGE, CURRENCY_CACHE_NAME, + ICON_UPDATE, + UPDATE_SETTINGS, DEFAULT_SETTINGS, + BUILTIN_UNIT_DEFINITIONS, + CUSTOM_DEFINITIONS_FILENAME) log = None @@ -29,7 +34,7 @@ Q = ureg.Quantity -def convert(query): +def convert(query, decimal_places=2): """Parse query, calculate and return conversion result Raises a `ValueError` if the query is not understood or is invalid (e.g. @@ -87,10 +92,12 @@ def convert(query): if to_unit is None: raise ValueError('Unknown unit : %s' % q2) conv = from_unit.to(to_unit) - # units = unicode(conv.units) - # if units.upper() in CURRENCIES: - # units = units.upper() - return '%0.2f %s' % (conv.magnitude, conv.units) + log.debug('%f %s' % (conv.magnitude, conv.units)) + + fmt = '%%0.%df %%s' % decimal_places + result = fmt % (conv.magnitude, conv.units) + + return result def main(wf): @@ -107,18 +114,30 @@ def main(wf): 'Use query `workflow:update` to install the new version', icon=ICON_UPDATE) + # Add custom units from workflow and user data + ureg.load_definitions(BUILTIN_UNIT_DEFINITIONS) + user_definitions = wf.datafile(CUSTOM_DEFINITIONS_FILENAME) + + # User's custom units + if os.path.exists(user_definitions): + ureg.load_definitions(user_definitions) + else: # Copy template to data dir + shutil.copy( + wf.workflowfile('{}.sample'.format(CUSTOM_DEFINITIONS_FILENAME)), + wf.datafile(CUSTOM_DEFINITIONS_FILENAME)) + # Load cached data exchange_rates = wf.cached_data(CURRENCY_CACHE_NAME, max_age=0) if exchange_rates: # Add exchange rates to conversion database - ureg.define('euros = [currency] = eur = EUR') + ureg.define('euro = [currency] = eur = EUR') for abbr, rate in exchange_rates.items(): ureg.define('{0} = eur / {1} = {2}'.format(abbr, rate, abbr.lower())) if not wf.cached_data_fresh(CURRENCY_CACHE_NAME, CURRENCY_CACHE_AGE): # Update currency rates - cmd = ['/usr/bin/python', wf.workflowfile('update_exchange_rates.py')] + cmd = ['/usr/bin/python', wf.workflowfile('currency.py')] run_in_background('update', cmd) if is_running('update'): @@ -134,7 +153,9 @@ def main(wf): conversion = None try: - conversion = convert(query) + conversion = convert(query, + decimal_places=wf.settings.get('decimal_places', + 2)) except UndefinedUnitError as err: log.critical('Unknown unit : %s', err.unit_names) error = 'Unknown unit : {}'.format(err.unit_names) diff --git a/src/currency.py b/src/currency.py index 8874539..649c478 100644 --- a/src/currency.py +++ b/src/currency.py @@ -14,46 +14,24 @@ from __future__ import print_function, unicode_literals import csv -from datetime import timedelta from itertools import izip_longest import re -from workflow import (Workflow, web, - ICON_WARNING, ICON_INFO, - MATCH_ALL, MATCH_ALLCHARS) +from workflow import Workflow, web from config import (CURRENCY_CACHE_NAME, - ICON_CURRENCY, + CURRENCY_CACHE_AGE, REFERENCE_CURRENCY, CURRENCIES, YAHOO_BASE_URL, SYMBOLS_PER_REQUEST) -wf = Workflow() -log = wf.logger +log = None parse_yahoo_response = re.compile(r'{}(.+)=X'.format(REFERENCE_CURRENCY)).match -def human_timedelta(td): - output = [] - d = {'day': td.days} - d['hour'], rem = divmod(td.seconds, 3600) - d['minute'], d['second'] = divmod(rem, 60) - for unit in ('day', 'hour', 'minute', 'second'): - i = d[unit] - if unit == 'second' and len(output): - # no seconds unless last update was < 1m ago - break - if i == 1: - output.append('1 %s' % unit) - elif i > 1: - output.append('%d %ss' % (i, unit)) - output.append('ago') - return ' '.join(output) - - def grouper(n, iterable, fillvalue=None): """Return iterable that groups ``iterable`` into groups of length ``n`` @@ -88,6 +66,7 @@ def load_yahoo_rates(symbols): count -= 1 continue parts.append('{}{}=X'.format(REFERENCE_CURRENCY, symbol)) + query = ','.join(parts) url = YAHOO_BASE_URL.format(query) @@ -102,18 +81,22 @@ def load_yahoo_rates(symbols): for row in csv.reader(lines): if not row: continue + name, rate = row m = parse_yahoo_response(name) - if not m: + + if not m: # Couldn't get symbol log.error('Invalid currency : {}'.format(name)) ycount += 1 continue symbol = m.group(1) rate = float(rate) - if rate == 0: + + if rate == 0: # Yahoo! returns 0.0 as rate for unsupported currencies log.error('No exchange rate for : {}'.format(name)) ycount += 1 continue + rates[symbol] = rate ycount += 1 @@ -141,39 +124,20 @@ def fetch_currency_rates(): def main(wf): - global log - log = wf.logger - query = '' - if len(wf.args): - query = wf.args[0] - - currencies = CURRENCIES.items() - - if query: - currencies = wf.filter(query, currencies, - key=lambda t: ' '.join(t), - match_on=MATCH_ALL ^ MATCH_ALLCHARS, - min_score=30) - - # currencies = filter_currencies(query) - if not currencies: - wf.add_item('No matching currencies found', - valid=False, icon=ICON_WARNING) - else: - if not query: # Show last update time - td = timedelta(seconds=wf.cached_data_age(CURRENCY_CACHE_NAME)) - # currencies_updated = datetime.now() - timedelta(seconds=age) - wf.add_item('Exchange rates updated %s' % human_timedelta(td), - valid=False, icon=ICON_INFO) - for name, abbr in currencies: - # wf.add_item(abbr, name, valid=False, icon='money.png') - wf.add_item('%s — %s' % (abbr, name), - 'Use the 3-letter currency code in conversions', - valid=False, icon=ICON_CURRENCY) - - wf.send_feedback() - return 0 + + log.debug('Fetching exchange rates from Yahoo! ...') + + exchange_rates = wf.cached_data(CURRENCY_CACHE_NAME, + fetch_currency_rates, + CURRENCY_CACHE_AGE) + + log.debug('Exchange rates updated.') + + for currency, rate in exchange_rates.items(): + wf.logger.debug('1 EUR = {0} {1}'.format(rate, currency)) if __name__ == '__main__': + wf = Workflow() + log = wf.logger wf.run(main) diff --git a/src/docopt.py b/src/docopt.py new file mode 100644 index 0000000..7b927e2 --- /dev/null +++ b/src/docopt.py @@ -0,0 +1,579 @@ +"""Pythonic command-line interface parser that will make you smile. + + * http://docopt.org + * Repository and issue-tracker: https://github.com/docopt/docopt + * Licensed under terms of MIT license (see LICENSE-MIT) + * Copyright (c) 2013 Vladimir Keleshev, vladimir@keleshev.com + +""" +import sys +import re + + +__all__ = ['docopt'] +__version__ = '0.6.2' + + +class DocoptLanguageError(Exception): + + """Error in construction of usage-message by developer.""" + + +class DocoptExit(SystemExit): + + """Exit in case user invoked program with incorrect arguments.""" + + usage = '' + + def __init__(self, message=''): + SystemExit.__init__(self, (message + '\n' + self.usage).strip()) + + +class Pattern(object): + + def __eq__(self, other): + return repr(self) == repr(other) + + def __hash__(self): + return hash(repr(self)) + + def fix(self): + self.fix_identities() + self.fix_repeating_arguments() + return self + + def fix_identities(self, uniq=None): + """Make pattern-tree tips point to same object if they are equal.""" + if not hasattr(self, 'children'): + return self + uniq = list(set(self.flat())) if uniq is None else uniq + for i, c in enumerate(self.children): + if not hasattr(c, 'children'): + assert c in uniq + self.children[i] = uniq[uniq.index(c)] + else: + c.fix_identities(uniq) + + def fix_repeating_arguments(self): + """Fix elements that should accumulate/increment values.""" + either = [list(c.children) for c in self.either.children] + for case in either: + for e in [c for c in case if case.count(c) > 1]: + if type(e) is Argument or type(e) is Option and e.argcount: + if e.value is None: + e.value = [] + elif type(e.value) is not list: + e.value = e.value.split() + if type(e) is Command or type(e) is Option and e.argcount == 0: + e.value = 0 + return self + + @property + def either(self): + """Transform pattern into an equivalent, with only top-level Either.""" + # Currently the pattern will not be equivalent, but more "narrow", + # although good enough to reason about list arguments. + ret = [] + groups = [[self]] + while groups: + children = groups.pop(0) + types = [type(c) for c in children] + if Either in types: + either = [c for c in children if type(c) is Either][0] + children.pop(children.index(either)) + for c in either.children: + groups.append([c] + children) + elif Required in types: + required = [c for c in children if type(c) is Required][0] + children.pop(children.index(required)) + groups.append(list(required.children) + children) + elif Optional in types: + optional = [c for c in children if type(c) is Optional][0] + children.pop(children.index(optional)) + groups.append(list(optional.children) + children) + elif AnyOptions in types: + optional = [c for c in children if type(c) is AnyOptions][0] + children.pop(children.index(optional)) + groups.append(list(optional.children) + children) + elif OneOrMore in types: + oneormore = [c for c in children if type(c) is OneOrMore][0] + children.pop(children.index(oneormore)) + groups.append(list(oneormore.children) * 2 + children) + else: + ret.append(children) + return Either(*[Required(*e) for e in ret]) + + +class ChildPattern(Pattern): + + def __init__(self, name, value=None): + self.name = name + self.value = value + + def __repr__(self): + return '%s(%r, %r)' % (self.__class__.__name__, self.name, self.value) + + def flat(self, *types): + return [self] if not types or type(self) in types else [] + + def match(self, left, collected=None): + collected = [] if collected is None else collected + pos, match = self.single_match(left) + if match is None: + return False, left, collected + left_ = left[:pos] + left[pos + 1:] + same_name = [a for a in collected if a.name == self.name] + if type(self.value) in (int, list): + if type(self.value) is int: + increment = 1 + else: + increment = ([match.value] if type(match.value) is str + else match.value) + if not same_name: + match.value = increment + return True, left_, collected + [match] + same_name[0].value += increment + return True, left_, collected + return True, left_, collected + [match] + + +class ParentPattern(Pattern): + + def __init__(self, *children): + self.children = list(children) + + def __repr__(self): + return '%s(%s)' % (self.__class__.__name__, + ', '.join(repr(a) for a in self.children)) + + def flat(self, *types): + if type(self) in types: + return [self] + return sum([c.flat(*types) for c in self.children], []) + + +class Argument(ChildPattern): + + def single_match(self, left): + for n, p in enumerate(left): + if type(p) is Argument: + return n, Argument(self.name, p.value) + return None, None + + @classmethod + def parse(class_, source): + name = re.findall('(<\S*?>)', source)[0] + value = re.findall('\[default: (.*)\]', source, flags=re.I) + return class_(name, value[0] if value else None) + + +class Command(Argument): + + def __init__(self, name, value=False): + self.name = name + self.value = value + + def single_match(self, left): + for n, p in enumerate(left): + if type(p) is Argument: + if p.value == self.name: + return n, Command(self.name, True) + else: + break + return None, None + + +class Option(ChildPattern): + + def __init__(self, short=None, long=None, argcount=0, value=False): + assert argcount in (0, 1) + self.short, self.long = short, long + self.argcount, self.value = argcount, value + self.value = None if value is False and argcount else value + + @classmethod + def parse(class_, option_description): + short, long, argcount, value = None, None, 0, False + options, _, description = option_description.strip().partition(' ') + options = options.replace(',', ' ').replace('=', ' ') + for s in options.split(): + if s.startswith('--'): + long = s + elif s.startswith('-'): + short = s + else: + argcount = 1 + if argcount: + matched = re.findall('\[default: (.*)\]', description, flags=re.I) + value = matched[0] if matched else None + return class_(short, long, argcount, value) + + def single_match(self, left): + for n, p in enumerate(left): + if self.name == p.name: + return n, p + return None, None + + @property + def name(self): + return self.long or self.short + + def __repr__(self): + return 'Option(%r, %r, %r, %r)' % (self.short, self.long, + self.argcount, self.value) + + +class Required(ParentPattern): + + def match(self, left, collected=None): + collected = [] if collected is None else collected + l = left + c = collected + for p in self.children: + matched, l, c = p.match(l, c) + if not matched: + return False, left, collected + return True, l, c + + +class Optional(ParentPattern): + + def match(self, left, collected=None): + collected = [] if collected is None else collected + for p in self.children: + m, left, collected = p.match(left, collected) + return True, left, collected + + +class AnyOptions(Optional): + + """Marker/placeholder for [options] shortcut.""" + + +class OneOrMore(ParentPattern): + + def match(self, left, collected=None): + assert len(self.children) == 1 + collected = [] if collected is None else collected + l = left + c = collected + l_ = None + matched = True + times = 0 + while matched: + # could it be that something didn't match but changed l or c? + matched, l, c = self.children[0].match(l, c) + times += 1 if matched else 0 + if l_ == l: + break + l_ = l + if times >= 1: + return True, l, c + return False, left, collected + + +class Either(ParentPattern): + + def match(self, left, collected=None): + collected = [] if collected is None else collected + outcomes = [] + for p in self.children: + matched, _, _ = outcome = p.match(left, collected) + if matched: + outcomes.append(outcome) + if outcomes: + return min(outcomes, key=lambda outcome: len(outcome[1])) + return False, left, collected + + +class TokenStream(list): + + def __init__(self, source, error): + self += source.split() if hasattr(source, 'split') else source + self.error = error + + def move(self): + return self.pop(0) if len(self) else None + + def current(self): + return self[0] if len(self) else None + + +def parse_long(tokens, options): + """long ::= '--' chars [ ( ' ' | '=' ) chars ] ;""" + long, eq, value = tokens.move().partition('=') + assert long.startswith('--') + value = None if eq == value == '' else value + similar = [o for o in options if o.long == long] + if tokens.error is DocoptExit and similar == []: # if no exact match + similar = [o for o in options if o.long and o.long.startswith(long)] + if len(similar) > 1: # might be simply specified ambiguously 2+ times? + raise tokens.error('%s is not a unique prefix: %s?' % + (long, ', '.join(o.long for o in similar))) + elif len(similar) < 1: + argcount = 1 if eq == '=' else 0 + o = Option(None, long, argcount) + options.append(o) + if tokens.error is DocoptExit: + o = Option(None, long, argcount, value if argcount else True) + else: + o = Option(similar[0].short, similar[0].long, + similar[0].argcount, similar[0].value) + if o.argcount == 0: + if value is not None: + raise tokens.error('%s must not have an argument' % o.long) + else: + if value is None: + if tokens.current() is None: + raise tokens.error('%s requires argument' % o.long) + value = tokens.move() + if tokens.error is DocoptExit: + o.value = value if value is not None else True + return [o] + + +def parse_shorts(tokens, options): + """shorts ::= '-' ( chars )* [ [ ' ' ] chars ] ;""" + token = tokens.move() + assert token.startswith('-') and not token.startswith('--') + left = token.lstrip('-') + parsed = [] + while left != '': + short, left = '-' + left[0], left[1:] + similar = [o for o in options if o.short == short] + if len(similar) > 1: + raise tokens.error('%s is specified ambiguously %d times' % + (short, len(similar))) + elif len(similar) < 1: + o = Option(short, None, 0) + options.append(o) + if tokens.error is DocoptExit: + o = Option(short, None, 0, True) + else: # why copying is necessary here? + o = Option(short, similar[0].long, + similar[0].argcount, similar[0].value) + value = None + if o.argcount != 0: + if left == '': + if tokens.current() is None: + raise tokens.error('%s requires argument' % short) + value = tokens.move() + else: + value = left + left = '' + if tokens.error is DocoptExit: + o.value = value if value is not None else True + parsed.append(o) + return parsed + + +def parse_pattern(source, options): + tokens = TokenStream(re.sub(r'([\[\]\(\)\|]|\.\.\.)', r' \1 ', source), + DocoptLanguageError) + result = parse_expr(tokens, options) + if tokens.current() is not None: + raise tokens.error('unexpected ending: %r' % ' '.join(tokens)) + return Required(*result) + + +def parse_expr(tokens, options): + """expr ::= seq ( '|' seq )* ;""" + seq = parse_seq(tokens, options) + if tokens.current() != '|': + return seq + result = [Required(*seq)] if len(seq) > 1 else seq + while tokens.current() == '|': + tokens.move() + seq = parse_seq(tokens, options) + result += [Required(*seq)] if len(seq) > 1 else seq + return [Either(*result)] if len(result) > 1 else result + + +def parse_seq(tokens, options): + """seq ::= ( atom [ '...' ] )* ;""" + result = [] + while tokens.current() not in [None, ']', ')', '|']: + atom = parse_atom(tokens, options) + if tokens.current() == '...': + atom = [OneOrMore(*atom)] + tokens.move() + result += atom + return result + + +def parse_atom(tokens, options): + """atom ::= '(' expr ')' | '[' expr ']' | 'options' + | long | shorts | argument | command ; + """ + token = tokens.current() + result = [] + if token in '([': + tokens.move() + matching, pattern = {'(': [')', Required], '[': [']', Optional]}[token] + result = pattern(*parse_expr(tokens, options)) + if tokens.move() != matching: + raise tokens.error("unmatched '%s'" % token) + return [result] + elif token == 'options': + tokens.move() + return [AnyOptions()] + elif token.startswith('--') and token != '--': + return parse_long(tokens, options) + elif token.startswith('-') and token not in ('-', '--'): + return parse_shorts(tokens, options) + elif token.startswith('<') and token.endswith('>') or token.isupper(): + return [Argument(tokens.move())] + else: + return [Command(tokens.move())] + + +def parse_argv(tokens, options, options_first=False): + """Parse command-line argument vector. + + If options_first: + argv ::= [ long | shorts ]* [ argument ]* [ '--' [ argument ]* ] ; + else: + argv ::= [ long | shorts | argument ]* [ '--' [ argument ]* ] ; + + """ + parsed = [] + while tokens.current() is not None: + if tokens.current() == '--': + return parsed + [Argument(None, v) for v in tokens] + elif tokens.current().startswith('--'): + parsed += parse_long(tokens, options) + elif tokens.current().startswith('-') and tokens.current() != '-': + parsed += parse_shorts(tokens, options) + elif options_first: + return parsed + [Argument(None, v) for v in tokens] + else: + parsed.append(Argument(None, tokens.move())) + return parsed + + +def parse_defaults(doc): + # in python < 2.7 you can't pass flags=re.MULTILINE + split = re.split('\n *(<\S+?>|-\S+?)', doc)[1:] + split = [s1 + s2 for s1, s2 in zip(split[::2], split[1::2])] + options = [Option.parse(s) for s in split if s.startswith('-')] + #arguments = [Argument.parse(s) for s in split if s.startswith('<')] + #return options, arguments + return options + + +def printable_usage(doc): + # in python < 2.7 you can't pass flags=re.IGNORECASE + usage_split = re.split(r'([Uu][Ss][Aa][Gg][Ee]:)', doc) + if len(usage_split) < 3: + raise DocoptLanguageError('"usage:" (case-insensitive) not found.') + if len(usage_split) > 3: + raise DocoptLanguageError('More than one "usage:" (case-insensitive).') + return re.split(r'\n\s*\n', ''.join(usage_split[1:]))[0].strip() + + +def formal_usage(printable_usage): + pu = printable_usage.split()[1:] # split and drop "usage:" + return '( ' + ' '.join(') | (' if s == pu[0] else s for s in pu[1:]) + ' )' + + +def extras(help, version, options, doc): + if help and any((o.name in ('-h', '--help')) and o.value for o in options): + print(doc.strip("\n")) + sys.exit() + if version and any(o.name == '--version' and o.value for o in options): + print(version) + sys.exit() + + +class Dict(dict): + def __repr__(self): + return '{%s}' % ',\n '.join('%r: %r' % i for i in sorted(self.items())) + + +def docopt(doc, argv=None, help=True, version=None, options_first=False): + """Parse `argv` based on command-line interface described in `doc`. + + `docopt` creates your command-line interface based on its + description that you pass as `doc`. Such description can contain + --options, , commands, which could be + [optional], (required), (mutually | exclusive) or repeated... + + Parameters + ---------- + doc : str + Description of your command-line interface. + argv : list of str, optional + Argument vector to be parsed. sys.argv[1:] is used if not + provided. + help : bool (default: True) + Set to False to disable automatic help on -h or --help + options. + version : any object + If passed, the object will be printed if --version is in + `argv`. + options_first : bool (default: False) + Set to True to require options preceed positional arguments, + i.e. to forbid options and positional arguments intermix. + + Returns + ------- + args : dict + A dictionary, where keys are names of command-line elements + such as e.g. "--verbose" and "", and values are the + parsed values of those elements. + + Example + ------- + >>> from docopt import docopt + >>> doc = ''' + Usage: + my_program tcp [--timeout=] + my_program serial [--baud=] [--timeout=] + my_program (-h | --help | --version) + + Options: + -h, --help Show this screen and exit. + --baud= Baudrate [default: 9600] + ''' + >>> argv = ['tcp', '127.0.0.1', '80', '--timeout', '30'] + >>> docopt(doc, argv) + {'--baud': '9600', + '--help': False, + '--timeout': '30', + '--version': False, + '': '127.0.0.1', + '': '80', + 'serial': False, + 'tcp': True} + + See also + -------- + * For video introduction see http://docopt.org + * Full documentation is available in README.rst as well as online + at https://github.com/docopt/docopt#readme + + """ + if argv is None: + argv = sys.argv[1:] + DocoptExit.usage = printable_usage(doc) + options = parse_defaults(doc) + pattern = parse_pattern(formal_usage(DocoptExit.usage), options) + # [default] syntax for argument is disabled + #for a in pattern.flat(Argument): + # same_name = [d for d in arguments if d.name == a.name] + # if same_name: + # a.value = same_name[0].value + argv = parse_argv(TokenStream(argv, DocoptExit), list(options), + options_first) + pattern_options = set(pattern.flat(Option)) + for ao in pattern.flat(AnyOptions): + doc_options = parse_defaults(doc) + ao.children = list(set(doc_options) - pattern_options) + #if any_options: + # ao.children += [Option(o.short, o.long, o.argcount) + # for o in argv if type(o) is Option] + extras(help, version, argv, doc) + matched, left, collected = pattern.fix().match(argv) + if matched and left == []: # better error message if left? + return Dict((a.name, a.value) for a in (pattern.flat() + collected)) + raise DocoptExit() diff --git a/src/info.plist b/src/info.plist index 14252cb..ee9e122 100644 --- a/src/info.plist +++ b/src/info.plist @@ -25,11 +25,22 @@ - 1F76517E-4F63-4286-BBBB-D8A6F19F6117 + 96CC03AC-25D6-4FDB-AA8B-FCF145F4D597 destinationuid - 6B9E2332-4E13-4547-8CD9-C3E4D307C878 + 14C63701-669A-43A3-BF6A-65A23759DA5F + modifiers + 0 + modifiersubtext + + + + EF92F243-C49B-4DFC-B0A4-72317B2457A7 + + + destinationuid + 96CC03AC-25D6-4FDB-AA8B-FCF145F4D597 modifiers 0 modifiersubtext @@ -63,7 +74,7 @@ runningsubtext Convertifying… script - python convert.py "{query}" + /usr/bin/python convert.py "{query}" subtext Convert a free-form quantity to other units title @@ -98,17 +109,25 @@ config - escaping - 127 - script - open README.html - type + lastpathcomponent + + onlyshowifquerypopulated + + output 0 + removeextension + + sticky + + text + {query} + title + Copied to Clipboard type - alfred.workflow.action.script + alfred.workflow.output.notification uid - 6B9E2332-4E13-4547-8CD9-C3E4D307C878 + 67960284-5834-4BB2-849E-E5CF3254D3CF version 0 @@ -116,20 +135,30 @@ config argumenttype - 2 + 1 + escaping + 102 keyword - convhelp + convinfo + queuedelay + 0 + queuemode + 1 + script + /usr/bin/python info.py "{query}" subtext - Open the Convert help file in your browser - text - View Convert Help File + View help file and supported currencies + title + Convert Information and Help + type + 0 withspace type - alfred.workflow.input.keyword + alfred.workflow.input.scriptfilter uid - 1F76517E-4F63-4286-BBBB-D8A6F19F6117 + EF92F243-C49B-4DFC-B0A4-72317B2457A7 version 0 @@ -139,55 +168,37 @@ lastpathcomponent onlyshowifquerypopulated - + output 0 removeextension sticky - text - {query} title - Copied to Clipboard + {query} type alfred.workflow.output.notification uid - 67960284-5834-4BB2-849E-E5CF3254D3CF + 14C63701-669A-43A3-BF6A-65A23759DA5F version 0 config - argumenttype - 1 escaping - 102 - keyword - convcurrencies - queuedelay 0 - queuemode - 1 - runningsubtext - Loading currencies… script - python currency.py "{query}" - subtext - Show list of currencies supported by Convert - title - Convert: Supported Currencies + /usr/bin/python info.py {query} type 0 - withspace - type - alfred.workflow.input.scriptfilter + alfred.workflow.action.script uid - 958A2D60-7569-45BC-A439-EE18BD094943 + 96CC03AC-25D6-4FDB-AA8B-FCF145F4D597 version 0 @@ -196,15 +207,15 @@ uidata - 1E12C11D-30DB-44A8-AD75-F5BE7F2DA451 + 14C63701-669A-43A3-BF6A-65A23759DA5F ypos - 10 + 250 - 1F76517E-4F63-4286-BBBB-D8A6F19F6117 + 1E12C11D-30DB-44A8-AD75-F5BE7F2DA451 ypos - 130 + 10 607FA0B2-94A5-48E4-96E9-0BFA8872F7EE @@ -216,12 +227,12 @@ ypos 130 - 6B9E2332-4E13-4547-8CD9-C3E4D307C878 + 96CC03AC-25D6-4FDB-AA8B-FCF145F4D597 ypos - 130 + 250 - 958A2D60-7569-45BC-A439-EE18BD094943 + EF92F243-C49B-4DFC-B0A4-72317B2457A7 ypos 250 diff --git a/src/info.py b/src/info.py new file mode 100755 index 0000000..ba5eca4 --- /dev/null +++ b/src/info.py @@ -0,0 +1,211 @@ +#!/usr/bin/python +# encoding: utf-8 +# +# Copyright © 2014 deanishe@deanishe.net +# +# MIT Licence. See http://opensource.org/licenses/MIT +# +# Created on 2014-12-26 +# + +"""info.py [options] [] + +Usage: + info.py [] + info.py (-h|--help) + info.py --openhelp + info.py --openunits + info.py --currencies [] + info.py --places + +Options: + -h, --help Show this message + --openhelp Open help file in default browser + --openunits Open custom units file in default editor + --currencies View/search supported currencies + --places Set decimal places + +""" + +from __future__ import print_function, unicode_literals, absolute_import + +from datetime import timedelta +import os +import shutil +import subprocess +import sys + +from workflow import (Workflow, + ICON_HELP, ICON_WARNING, ICON_INFO, ICON_SETTINGS, + MATCH_ALL, MATCH_ALLCHARS) + +from config import (ICON_CURRENCY, + CURRENCY_CACHE_NAME, + CUSTOM_DEFINITIONS_FILENAME, + CURRENCIES, + DECIMAL_PLACES_DEFAULT) + +log = None + +DELIMITER = '⟩' + +ALFRED_AS = 'tell application "Alfred 2" to search "convinfo"' + + +def human_timedelta(td): + """Return relative time (past) in human-readable format + + :param td: :class:`datetime.timedelta` + :returns: Human-readable Unicode string + + """ + + output = [] + d = {'day': td.days} + d['hour'], rem = divmod(td.seconds, 3600) + d['minute'], d['second'] = divmod(rem, 60) + + for unit in ('day', 'hour', 'minute', 'second'): + i = d[unit] + + if unit == 'second' and len(output): + # no seconds unless last update was < 1m ago + break + + if i == 1: + output.append('1 %s' % unit) + + elif i > 1: + output.append('%d %ss' % (i, unit)) + + output.append('ago') + return ' '.join(output) + + +def main(wf): + + from docopt import docopt + + args = docopt(__doc__, wf.args) + + log.debug('args : {!r}'.format(args)) + + query = args.get('') + + if args.get('--openhelp'): + subprocess.call(['open', wf.workflowfile('README.html')]) + return 0 + + if args.get('--openunits'): + path = wf.datafile(CUSTOM_DEFINITIONS_FILENAME) + if not os.path.exists(path): + shutil.copy( + wf.workflowfile('{}.sample'.format( + CUSTOM_DEFINITIONS_FILENAME)), + path) + + subprocess.call(['open', path]) + return 0 + + if args.get('--places'): + value = int(query) + log.debug('Setting `decimal_places` to {!r}'.format(value)) + wf.settings['decimal_places'] = value + print('Set decimal places to {}'.format(value)) + # subprocess.call(['osascript', '-e', ALFRED_AS]) + return 0 + + if not query or not query.strip(): + wf.add_item('View Help File', + 'Open help file in your browser', + valid=True, + arg='--openhelp', + icon=ICON_HELP) + + wf.add_item('View Supported Currencies', + 'View and search list of supported currencies', + autocomplete=' currencies {} '.format(DELIMITER), + icon=ICON_CURRENCY) + + wf.add_item(('Decimal Places in Results ' + '(current : {})'.format(wf.settings.get( + 'decimal_places', + DECIMAL_PLACES_DEFAULT))), + 'View and search list of supported currencies', + autocomplete=' places {} '.format(DELIMITER), + icon=ICON_SETTINGS) + + wf.add_item('Edit Custom Units', + 'Add and edit your own custom units', + valid=True, + arg='--openunits', + icon='icon.png') + + wf.send_feedback() + return 0 + + else: # Currencies or decimal places + if query.endswith(DELIMITER): # User deleted trailing space + subprocess.call(['osascript', '-e', ALFRED_AS]) + return 0 + + mode, query = [s.strip() for s in query.split(DELIMITER)] + + if mode == 'currencies': + + currencies = sorted([(name, symbol) for (symbol, name) + in CURRENCIES.items()]) + + if query: + currencies = wf.filter(query, currencies, + key=lambda t: ' '.join(t), + match_on=MATCH_ALL ^ MATCH_ALLCHARS, + min_score=30) + + else: # Show last update time + age = wf.cached_data_age(CURRENCY_CACHE_NAME) + if age > 0: # Exchange rates in cache + td = timedelta(seconds=age) + wf.add_item('Exchange rates updated {}'.format( + human_timedelta(td)), + icon=ICON_INFO) + + if not currencies: + wf.add_item('No matching currencies', + 'Try a different query', + icon=ICON_WARNING) + + for name, symbol in currencies: + wf.add_item('{} // {}'.format(name, symbol), + 'Use `{}` in conversions'.format(symbol), + icon=ICON_CURRENCY) + + wf.send_feedback() + + elif mode == 'places': + + if query: + if not query.isdigit(): + wf.add_item('Invalid number : {}'.format(query), + 'Please enter a number', + icon=ICON_WARNING) + else: + wf.add_item('Set decimal places to : {}'.format(query), + 'Hit `ENTER` to save', + valid=True, + arg='--places {}'.format(query), + icon=ICON_SETTINGS) + else: + wf.add_item('Enter a number of decimal places', + 'Current number is {}'.format( + wf.settings.get('decimal_places', + DECIMAL_PLACES_DEFAULT)), + icon=ICON_INFO) + + wf.send_feedback() + + +if __name__ == '__main__': + wf = Workflow() + log = wf.logger + sys.exit(wf.run(main)) diff --git a/src/unit_definitions.txt b/src/unit_definitions.txt new file mode 100644 index 0000000..c0e76d6 --- /dev/null +++ b/src/unit_definitions.txt @@ -0,0 +1,5 @@ +# Barrel of oil equivalent +barrel_of_oil_equivalent = 169.902 m**3 = boe + +# Million standard cubic feed +MMscf = 28.31685 l * 1000000 = mmscf diff --git a/src/unit_definitions.txt.sample b/src/unit_definitions.txt.sample new file mode 100644 index 0000000..4752c51 --- /dev/null +++ b/src/unit_definitions.txt.sample @@ -0,0 +1,6 @@ +# Add your custom definitions to this file. See the Pint documentation for +# information on the necessary format: +# http://pint.readthedocs.org/en/latest/defining.html +# You can see the default definitions built in to Pint here: +# https://github.com/hgrecco/pint/blob/master/pint/default_en.txt + diff --git a/src/update_exchange_rates.py b/src/update_exchange_rates.py deleted file mode 100644 index d7ca799..0000000 --- a/src/update_exchange_rates.py +++ /dev/null @@ -1,40 +0,0 @@ -#!/usr/bin/env python -# encoding: utf-8 -# -# Copyright © 2014 deanishe@deanishe.net -# -# MIT Licence. See http://opensource.org/licenses/MIT -# -# Created on 2014-02-25 -# - -""" -Runs as daemon process in the background and updates exchange rates. -""" - -from __future__ import print_function, unicode_literals - -from config import CURRENCY_CACHE_NAME, CURRENCY_CACHE_AGE -from currency import fetch_currency_rates -from workflow import Workflow - -log = None - - -def main(wf): - # Insert delay to check info message is posted in Alfred - # import time - # time.sleep(10) - wf.logger.debug('Fetching exchange rates from ECB ...') - exchange_rates = wf.cached_data(CURRENCY_CACHE_NAME, - fetch_currency_rates, - CURRENCY_CACHE_AGE) - wf.logger.debug('Exchange rates updated.') - for currency, rate in exchange_rates.items(): - wf.logger.debug('1 EUR = {0} {1}'.format(rate, currency)) - - -if __name__ == '__main__': - wf = Workflow() - log = wf.logger - wf.run(main) diff --git a/src/version b/src/version index ea710ab..415b19f 100644 --- a/src/version +++ b/src/version @@ -1 +1 @@ -1.2 \ No newline at end of file +2.0 \ No newline at end of file