Skip to content

Commit

Permalink
V2
Browse files Browse the repository at this point in the history
Major change in internal conversion definition format - may affect users who customized it. Should not affect regular users. Can now customize using cvt.init
  • Loading branch information
DrorHarari committed Nov 1, 2019
1 parent 11b627f commit a867179
Show file tree
Hide file tree
Showing 4 changed files with 340 additions and 217 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,41 @@ You don't need to remember the measure names - a list of measure names will be o

![Example: see mass measure units and their conversion rules](images/example-measure.png?raw=true)

## Customizing Existing Conversions ##

You can edit the Cvt.ini configuration file and add units to existing measures or add aliases. To do so, just add a section based on the following pattern:

```
[unit/{measure name}/{unit name}]
factor = {expression to multiple the main unit by to get this unit}
aliases = {comma separate additional aliases}
offset = {number to substract after multiplying by factor}
inverse = {if true use the inverse of the factor}
```

For example, to add a "Finger" distance unit which is equivalent to 2 cm with the alias 'fg' we can use the following definition (note that for Distance, the main unit is meter as can be seen by typing DISTANCE+<tab> in Keypirinha)

```
[unit/Distance/Finger]
factor = 2/100
aliases = fg
```

To add an alias "hdm" for Centimeters unit of distance measure use the following (note that the unit must be specified with exdact case):
```
[unit/distance/Centimetres]
aliases = hdm
```

## Customizing Conversions ##

Cvt lets you customize the measures and units it supports. To customize the list, enter the "Cvt: Customize coversions" action in the box - this will place a copy of the conversion definition file cvtdefs.json in the user configuration directory (`Keypirinha\portable\Profile\User`). Make your changes to the measure or units definitions and then enter "Cvt: Reload custom coversions" action in the box.

When a custom conversion file is used, the built-in conversion file is ignored so you won't see new measures and units that come with Cvt.

If you want to create a conversion definition file that will add measures for a specific locale, you can use the name ```cvtdefs-{locale-name}.json```', for example the file ``cvtdefs-ja_JP.json``` will be loaded in addition to what's in ```cvtdefs.json' when running on Japanese machine.

## Installation ##

The easiest way to install Cvt is to use the [PackageControl](https://github.com/ueffel/Keypirinha-PackageControl) plugin's InstallPackage command.
Expand All @@ -69,6 +98,13 @@ For manual installation simply download the cvt.keypirinha-package file from the

## Release Notes ##

**V2.0.0**
- BERAKING CHANGE. The format of the cvtdefs.json was simplified to enable conversion customizations via the Cvt.ini configuration file. The conversion from the old format is simple but in most cases, if the customization was just about adding some units, then the now just those units need to be added.
- New ``cvtdefs-{locale-name}.json```` pattern was added.
- New Cvt.ini boolean configuration item 'debug' added to the main section to troubleshoot conversion definition.
- New Cvt.ini string configuration item 'locale' added to the main section to control the locale-specific version of ````cvtdefs-{locale}.json`` to load.
- Now it is possible to customize existing conversions using the Cvt.ini configuration file (see there for examples).

**V1.0.3**
- Units with uppercase name where not matched on input. Fixed.
- Internal work, added debug message to ease troubelshooting.
Expand Down
33 changes: 29 additions & 4 deletions cvt.ini
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,41 @@
[main]
# Plugin's main configuration section.

#Can Cvt load a user provided measurement definition file?
# Troubleshooting conversions (useful when trying to customize)
#debug = false

# Can Cvt load a user provided measurement definition file?
#
# * Cvt uses a built-in measurement definition file called cvtdefs.json
# * You can place a customized version of this file in the User folder
# * If you add such file, a Reload command is added for convenience
# * It is useful for extending and experimenting with Cvt
# * If you have generally useful measurements to add - let me know on Cvt's github

# The display name of the default item
# Default: "Cvt: "
#item_label = "Cvt: "
# Adding a custom unit to existing measurement is possible by adding a section to this
# file in the following format:
#
# [unit/{measure name}/{unit name}]
# factor = {expression to multiple the main unit by to get this unit}
# aliases = {comma separate additional aliases}
# offset = {number to substract after multiplying by factor}
# inverse = {if true use the inverse of the factor}
#
# To add a "Finger" distance unit which is equivalent to 2 cm with the alias
# 'fg' we can use the following definition (note that for Distance, the main
# unit is meter as can be seen by typing DISTANCE+<tab> in Keypirinha)
#
#[unit/Distance/Finger]
#factor = 2/100
#aliases = fg

#
# To add an alias "hdm" for Centimeters unit of distance measure use the
# following (note that the unit must be specified with exdact case):
#
#[unit/distance/Centimetres]
#aliases = hdm


[var]
# As in every Keypirinha's configuration file, you may optionally include a
Expand Down
161 changes: 122 additions & 39 deletions cvt.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,19 @@
from pathlib import Path
import os
import sys
import locale
from .lib.safeeval import Parser

class Cvt(kp.Plugin):
ITEMCAT_RESULT = kp.ItemCategory.USER_BASE + 1
ITEMCAT_RELOAD_DEFS = kp.ItemCategory.USER_BASE + 2
ITEMCAT_CREATE_CUSTOM_DEFS = kp.ItemCategory.USER_BASE + 3

CONFIG_SECTION_MAIN = "main"
ITEM_LABEL_PREFIX = "Cvt: "
UNIT_SECTION_PREFIX = "unit/"

CVTDEF_FILE = "cvtdefs.json"
CVTDEF_LOCALE_FILE = "cvtdefs-{}.json"
# Input parser definition
RE_NUMBER = r'(?P<number>[-+]?[0-9]+(?:\.?[0-9]+)?(?:[eE][-+]?[0-9]+)?)'
RE_FROM = r'(?P<from>[a-zA-Z]+[a-zA-Z0-9/]*)'
Expand All @@ -27,32 +29,119 @@ class Cvt(kp.Plugin):
def __init__(self):
super().__init__()

def load_conversions(self):
def read_defs(self, defs_file):
defs = None
try:
cvtdefs = os.path.join(kp.user_config_dir(), self.CVTDEF_FILE)
# Either file exist in the user profile dir
cvtdefs = os.path.join(kp.user_config_dir(), defs_file)
if os.path.exists(cvtdefs):
self.info(f"Loading custom conversion definition file '{cvtdefs}'")
self.customized_config = True
with open(cvtdefs, "r", encoding="utf-8") as f:
defs = json.load(f)
else:
self.customized_config = False
cvtdefs = os.path.join("data/",self.CVTDEF_FILE)
defs_text = self.load_text_resource(cvtdefs)
defs = json.loads(defs_text)
else: # ... or it may be in the plugin
try:
self.customized_config = False
cvtdefs = os.path.join("data/", defs_file)
defs_text = self.load_text_resource(cvtdefs)
defs = json.loads(defs_text)
self.dbg(f"Loaded internal conversion definitions '{cvtdefs}'")
except Exception as exc:
defs = { "measures" : {} }
self.dbg(f"Did not load internal definitions file '{cvtdefs}', {exc}")
pass
except Exception as exc:
self.warn(f"Failed to load definitions file '{cvtdefs}', {exc}")
return

self.measures = {measure["name"] : measure for measure in defs["measures"]}
return defs

# Load measures, merging into existing ones
def add_defs(self, defs):
if "measures" in defs:
def_measures = defs["measures"]
for new_measure_name,new_measure in def_measures.items():
new_measure_name = new_measure_name.lower()
if not new_measure_name in self.measures:
self.measures[new_measure_name] = new_measure
measure = self.measures[new_measure_name]
for new_unit_name,new_unit in new_measure["units"].items():
if not new_unit_name in measure["units"]:
measure["units"][new_unit_name] = new_unit
else:
if new_unit["factor"] != measure["units"][new_unit_name]["factor"]:
self.warn(f"Adding aliases to existing unit {new_unit_name} cannot change the unit factor")
for alias in new_unit["aliases"]:
alias = alias.lower()
if alias in self.all_units:
self.warn(f"Alias {alias} is defined multiple times for measure {new_measure_name}")
else:
unit = measure["units"][new_unit_name]
if not alias in unit["aliases"]:
unit["aliases"] = unit["aliases"] + [alias]
self.all_units[alias] = measure

# Units and aliaes can be customized in the cvt.ini file:
# [unit/Distance/Finger]
# factor = 0.02
# aliases = fg
# offset = 0
# inverse" = false
def read_setting_defs(self):
defs = { "measures": { } }
measures = defs["measures"]
for section in self.settings.sections():
if not section.lower().startswith(self.UNIT_SECTION_PREFIX):
continue

measure_name, unit_name = section[len(self.UNIT_SECTION_PREFIX):].strip().split("/", 1)
if not measure_name in measures:
measures[measure_name] = { "units": { }}
measure = measures[measure_name]

if not unit_name in measure["units"]:
measure["units"][unit_name] = { "aliases": [] }
unit = measure["units"][unit_name]

unit["factor"] = self.settings.get_stripped("factor", section=section, fallback="1.0")

offset = self.settings.get_stripped("offset", section=section, fallback=None)
if offset:
unit["offset"] = self.settings.get_stripped("offset", section=section, fallback=None)

inverse = self.settings.get_bool("inverse", section=section, fallback=None)
if inverse:
unit["inverse"] = self.settings.get_bool("inverse", section=section, fallback=None)

aliases = self.settings.get_stripped("aliases", section=section, fallback=None)
if aliases:
unit["aliases"] = unit["aliases"] + self.settings.get_stripped('aliases', section=section, fallback=None).split(",")

self.dbg(f"Added unit {unit_name} for measure {measure_name} as:\n{repr(unit)}")

return defs

def reconfigure(self):
self.settings = self.load_settings()
self._debug = self.settings.get_bool("debug", "main", False)
self.dbg("CVT: Reloading. Debug enabled")

self.all_units = {}
for measure in defs["measures"]:
for unit in measure["units"]:
for alias in unit["aliases"]:
alias = alias.lower()
if alias in self.all_units:
self.warn(f"Alias {alias} is defined multiple times")
self.all_units[alias] = measure
self.measures = {}

defs = self.read_defs(self.CVTDEF_FILE)
if defs:
self.add_defs(defs)

locale_name = self.settings.get_bool("locale", "main", locale.getdefaultlocale()[0])

locale_specific_def = self.CVTDEF_LOCALE_FILE.format(locale_name)
defs = self.read_defs(locale_specific_def)
if defs:
self.add_defs(defs)

defs = self.read_setting_defs()
if defs:
self.add_defs(defs)

def evaluate_expr(self, expr):
try:
Expand All @@ -64,10 +153,8 @@ def evaluate_expr(self, expr):
def on_start(self):
self.input_parser = re.compile(self.INPUT_PARSER)
self.safeparser = Parser()
self.settings = self.load_settings()
#self._debug = True

self.load_conversions()
self.reconfigure()

self.set_actions(self.ITEMCAT_RESULT, [
self.create_action(
Expand Down Expand Up @@ -95,23 +182,19 @@ def on_deactivated(self):

def on_events(self, flags):
if flags & kp.Events.PACKCONFIG:
self.load_conversions()
self.reconfigure()
self.on_catalog()

def on_catalog(self):
self.dbg(f"In on_catalog")
catalog = []

settings = self.load_settings()
self.ITEM_LABEL_PREFIX = settings.get("item_label", section=self.CONFIG_SECTION_MAIN, fallback=self.ITEM_LABEL_PREFIX, unquote=True)

# To discover measures and units, type CVT then proposed supported measures
for name,measure in self.measures.items():
catalog.append(self.create_item(
category=kp.ItemCategory.REFERENCE,
label=self.ITEM_LABEL_PREFIX + measure["name"],
label=self.ITEM_LABEL_PREFIX + name.title(),
short_desc=measure["desc"],
target=measure["name"],
target=name,
args_hint=kp.ItemArgsHint.REQUIRED,
hit_hint=kp.ItemHitHint.NOARGS))

Expand Down Expand Up @@ -172,16 +255,16 @@ def on_suggest(self, user_input, items_chain):
measure = self.measures[items_chain[-1].target()]
self.dbg(f"No parsed input, measure = {measure}")

for unit in measure["units"]:
for unit_name,unit in measure["units"].items():
conv_hint = f"factor {unit['factor']}"
if "offset" in unit:
conv_hint = conv_hint + f", offset {unit['offset']}"
self.dbg(f"Added suggestion for unit '{unit['name']}' conv_hint = {conv_hint}")
self.dbg(f"Added suggestion for unit '{unit_name}' conv_hint = {conv_hint}")
suggestions.append(self.create_item(
category=kp.ItemCategory.REFERENCE,
label=",".join(unit["aliases"]),
short_desc=f'Conversion unit {unit["name"]}, {conv_hint}',
target=unit["name"],
short_desc=f'Conversion unit {unit_name}, {conv_hint}',
target=unit_name,
args_hint=kp.ItemArgsHint.FORBIDDEN,
hit_hint=kp.ItemHitHint.IGNORE))

Expand Down Expand Up @@ -214,30 +297,30 @@ def on_suggest(self, user_input, items_chain):

check_from_unit_match = lambda u: not in_from or any([comperator(in_from, alias) for alias in u["aliases"]])
check_to_unit_match = lambda u: not in_to or any([comperator(in_to, alias) for alias in u["aliases"]])
units = list(filter(check_from_unit_match, measure["units"]))
units = list(filter(check_from_unit_match, measure["units"].values()))

if len(units) == 0:
comperator = cmp_inexact
units = list(filter(check_from_unit_match, measure["units"]))
units = list(filter(check_from_unit_match, measure["units"].values()))

self.dbg(f"#units matched = {len(units)}")
if len(units) == 1:
# At this point we know the measure and the from unit
# We propose the target units (filtered down if given to_unit)
from_unit = units[0]

for unit in measure["units"]:
self.dbg(f"unit = {unit['name']}")
for unit_name,unit in measure["units"].items():
self.dbg(f"unit = {unit_name}")
comperator = cmp_exact if in_done_to else cmp_inexact
if not check_to_unit_match(unit):
continue

self.dbg(f"Added unit = {unit['name']}")
self.dbg(f"Added unit = {unit_name}")
converted = self.do_conversion(in_number, from_unit, unit)
suggestions.append(self.create_item(
category=self.ITEMCAT_RESULT,
label=format(converted,".5g"),
short_desc=f'{unit["name"]} ({",".join(unit["aliases"])})',
short_desc=f'{unit_name} ({",".join(unit["aliases"])})',
target=format(converted,".5g"),
args_hint=kp.ItemArgsHint.FORBIDDEN,
hit_hint=kp.ItemHitHint.IGNORE,
Expand All @@ -249,7 +332,7 @@ def on_execute(self, item, action):
if item and item.category() == self.ITEMCAT_RESULT:
kpu.set_clipboard(item.data_bag())
elif item and item.category() == self.ITEMCAT_RELOAD_DEFS:
self.load_conversions()
self.reconfigure()
self.on_catalog()
elif item and item.category() == self.ITEMCAT_CREATE_CUSTOM_DEFS:
try:
Expand All @@ -263,7 +346,7 @@ def on_execute(self, item, action):
f.write(builtin_cvtdefs_text)
f.close()
kpu.explore_file(custom_cvtdefs)
self.load_conversions()
self.reconfigure()
self.on_catalog()
except Exception as exc:
self.warn(f"Failed to create custom conversion definition file '{custom_cvtdefs}', {exc}")
Loading

0 comments on commit a867179

Please sign in to comment.