diff --git a/DEBIAN/control b/DEBIAN/control index 4e077bbf..e90eb662 100644 --- a/DEBIAN/control +++ b/DEBIAN/control @@ -1,5 +1,5 @@ Package: input-remapper -Version: 1.6.0-beta +Version: 2.0.0-rc Architecture: all Maintainer: Sezanzeb Depends: build-essential, libpython3-dev, libdbus-1-dev, python3, python3-setuptools, python3-evdev, python3-pydbus, python3-gi, gettext, python3-cairo, libgtk-3-0, libgtksourceview-4-dev, python3-pydantic diff --git a/README.md b/README.md index 08ce3b74..6ac4e605 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

-

Input Remapper (Beta)

+

Input Remapper

An easy to use tool to change the behaviour of your input devices.
@@ -14,9 +14,9 @@

- - - + +   +

## Installation @@ -36,7 +36,7 @@ or install the latest changes via: sudo apt install git python3-setuptools gettext git clone https://github.com/sezanzeb/input-remapper.git cd input-remapper && ./scripts/build.sh -sudo apt install ./dist/input-remapper-1.6.0-beta.deb +sudo apt install -f ./dist/input-remapper-2.0.0-rc.deb ``` input-remapper is available in [Debian](https://tracker.debian.org/pkg/input-remapper) @@ -50,6 +50,8 @@ Python packages need to be installed globally for the service to be able to impo Conda can cause problems due to changed python paths and versions. +If it doesn't seem to install, you can also try `sudo python3 setup.py install` + ```bash sudo pip install evdev -U # If newest version not in distros repo sudo pip uninstall key-mapper # In case the old package is still installed @@ -58,11 +60,14 @@ sudo systemctl enable input-remapper sudo systemctl restart input-remapper ``` -If it doesn't seem to install, you can also try `sudo python3 setup.py install` +## Migrating beta configs to version 2 -##### Beta +By default, Input Remapper will not migrate configurations from the beta. +If you want to use those you will need to copy them manually. -The `beta` branch contains features that still require work, but that are ready for testing. It uses a different -config path, so your presets won't break. `input-remapper-beta-git` can be installed from the AUR. If you are -facing problems, please open up an [issue](https://github.com/sezanzeb/input-remapper/issues). +```bash +rm ~/.config/input-remapper-2 -r +cp ~/.config/input-remapper/beta_1.6.0-beta ~/.config/input-remapper-2 -r +``` +Then start input-remapper diff --git a/inputremapper/configs/migrations.py b/inputremapper/configs/migrations.py index c3d8eff0..08a1a9c4 100644 --- a/inputremapper/configs/migrations.py +++ b/inputremapper/configs/migrations.py @@ -54,7 +54,7 @@ from inputremapper.configs.preset import Preset from inputremapper.configs.system_mapping import system_mapping from inputremapper.injection.global_uinputs import global_uinputs from inputremapper.injection.macros.parse import is_this_a_macro -from inputremapper.logger import logger, VERSION, IS_BETA +from inputremapper.logger import logger, VERSION from inputremapper.user import HOME @@ -169,12 +169,12 @@ def _update_version(): json.dump(config, file, indent=4) -def _rename_config(new_path=CONFIG_PATH): +def _rename_to_input_remapper(): """Rename .config/key-mapper to .config/input-remapper.""" old_config_path = os.path.join(HOME, ".config/key-mapper") - if not os.path.exists(new_path) and os.path.exists(old_config_path): - logger.info("Moving %s to %s", old_config_path, new_path) - shutil.move(old_config_path, new_path) + if not os.path.exists(CONFIG_PATH) and os.path.exists(old_config_path): + logger.info("Moving %s to %s", old_config_path, CONFIG_PATH) + shutil.move(old_config_path, CONFIG_PATH) def _find_target(symbol): @@ -429,19 +429,31 @@ def _convert_to_individual_mappings(): migrated_preset.save() -def _copy_to_beta(): - if os.path.exists(CONFIG_PATH) or not IS_BETA: +def _copy_to_v2(): + """Move the beta config to the v2 path, or copy the v1 config to the v2 path.""" + # TODO test + if os.path.exists(CONFIG_PATH): # don't copy to already existing folder - # users should delete the beta folder if they need to + # users should delete the input-remapper-2 folder if they need to return - regular_path = os.path.join(*os.path.split(CONFIG_PATH)[:-1]) - # workaround to maker sure the rename from key-mapper to input-remapper - # does not move everythig to the beta folder - _rename_config(regular_path) - if os.path.exists(regular_path): - logger.debug("copying all from %s to %s", regular_path, CONFIG_PATH) - shutil.copytree(regular_path, CONFIG_PATH) + # prioritize the v1 configs over beta configs + old_path = os.path.join(HOME, ".config/input-remapper") + if os.path.exists(os.path.join(old_path, "config.json")): + # no beta path, only old presets exist. COPY to v2 path, which will then be + # migrated by the various migrations. + logger.debug("copying all from %s to %s", old_path, CONFIG_PATH) + shutil.copytree(old_path, CONFIG_PATH) + return + + # if v1 configs don't exist, try to find beta configs. + beta_path = os.path.join(HOME, ".config/input-remapper/beta_1.6.0-beta") + if os.path.exists(beta_path): + # There has never been a different version than "1.6.0-beta" in beta, so we + # only need to check for that exact directory + # already migrated, possibly new presets in them, move to v2 path + logger.debug("moving %s to %s", beta_path, CONFIG_PATH) + shutil.move(beta_path, CONFIG_PATH) def _remove_logs(): @@ -459,8 +471,12 @@ def _remove_logs(): def migrate(): """Migrate config files to the current release.""" - _copy_to_beta() + _rename_to_input_remapper() + + _copy_to_v2() + v = config_version() + if v < pkg_resources.parse_version("0.4.0"): _config_suffix() _preset_path() @@ -468,9 +484,6 @@ def migrate(): if v < pkg_resources.parse_version("1.2.2"): _mapping_keys() - if v < pkg_resources.parse_version("1.3.0"): - _rename_config() - if v < pkg_resources.parse_version("1.4.0"): global_uinputs.prepare_all() _add_target() diff --git a/inputremapper/configs/paths.py b/inputremapper/configs/paths.py index aa80cfce..54ac5ccd 100644 --- a/inputremapper/configs/paths.py +++ b/inputremapper/configs/paths.py @@ -26,12 +26,11 @@ import os import shutil from typing import List, Union, Optional -from inputremapper.logger import logger, VERSION, IS_BETA +from inputremapper.logger import logger, VERSION from inputremapper.user import USER, HOME -rel_path = ".config/input-remapper" -if IS_BETA: - rel_path = os.path.join(rel_path, f"beta_{VERSION}") +rel_path = ".config/input-remapper-2" + CONFIG_PATH = os.path.join(HOME, rel_path) @@ -44,7 +43,7 @@ def chown(path): shutil.chown(path, user=USER) -def touch(path: os.PathLike, log=True): +def touch(path: Union[str, os.PathLike], log=True): """Create an empty file and all its parent dirs, give it to the user.""" if str(path).endswith("/"): raise ValueError(f"Expected path to not end with a slash: {path}") @@ -143,6 +142,6 @@ def get_preset_path(group_name: Optional[str] = None, preset: Optional[str] = No return os.path.join(presets_base, group_name, preset) -def get_config_path(*paths): +def get_config_path(*paths) -> str: """Get a path in ~/.config/input-remapper/.""" return os.path.join(CONFIG_PATH, *paths) diff --git a/inputremapper/configs/preset.py b/inputremapper/configs/preset.py index c31a09b8..8a17812a 100644 --- a/inputremapper/configs/preset.py +++ b/inputremapper/configs/preset.py @@ -296,34 +296,17 @@ class Preset(Generic[MappingModel]): logger.error("unable to decode json file: %s", self.path) return mappings - if isinstance(preset_list, dict): - # todo: remove this before merge into main - # adds compatibility with older beta versions - def str_to_cfg(string): - config = [] - for event_str in string.split("+"): - type_, code, analog_threshold = event_str.split(",") - config.append( - { - "type": type_, - "code": code, - "analog_threshold": analog_threshold, - } - ) - return config - - for combination_string, mapping_dict in preset_list.items(): - mapping_dict["input_combination"] = str_to_cfg(combination_string) - preset_list = list(preset_list.values()) - for mapping_dict in preset_list: + if not isinstance(mapping_dict, dict): + logger.error("Expected mapping to be a dict: %s", mapping_dict) + continue + try: mapping = self._mapping_factory(**mapping_dict) - except ValidationError as error: - print(mapping_dict) + except Exception as error: logger.error( "failed to Validate mapping for %s: %s", - mapping_dict["input_combination"], + mapping_dict.get("input_combination"), error, ) continue diff --git a/inputremapper/logger.py b/inputremapper/logger.py index ca46a579..38bca3c8 100644 --- a/inputremapper/logger.py +++ b/inputremapper/logger.py @@ -244,7 +244,7 @@ logging.getLogger("asyncio").setLevel(logging.WARNING) # using pkg_resources to figure out the version fails in many cases, # so we hardcode it instead -VERSION = "1.6.0-beta" +VERSION = "2.0.0-rc" EVDEV_VERSION = None try: EVDEV_VERSION = pkg_resources.require("evdev")[0].version diff --git a/readme/development.md b/readme/development.md index 4a195490..481262f5 100644 --- a/readme/development.md +++ b/readme/development.md @@ -72,7 +72,7 @@ ssh/login into a debian/ubuntu environment ./scripts/build.sh ``` -This will generate `input-remapper/deb/input-remapper-1.6.0-beta.deb` +This will generate `input-remapper/deb/input-remapper-2.0.0-rc.deb` ## Badges @@ -121,3 +121,4 @@ https://miro.com/app/board/uXjVPLa8ilM=/?share_link_id=272180986764 - [GNOME HIG](https://developer.gnome.org/hig/stable/) - [GtkSource Example](https://github.com/wolfthefallen/py-GtkSourceCompletion-example) - [linux/input-event-codes.h](https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h) +- [Screenshot Guidelines](https://www.freedesktop.org/software/appstream/docs/chap-Quickstart.html) diff --git a/readme/screenshot.png b/readme/screenshot.png index 770b61db..a8a75da8 100644 Binary files a/readme/screenshot.png and b/readme/screenshot.png differ diff --git a/readme/screenshot_2.png b/readme/screenshot_2.png index f250ebf8..0bed2c09 100644 Binary files a/readme/screenshot_2.png and b/readme/screenshot_2.png differ diff --git a/scripts/build.sh b/scripts/build.sh index 1bc9a73d..91eba4a0 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -9,8 +9,8 @@ build_deb() { mv build/deb/usr/local/lib/python3.*/ build/deb/usr/lib/python3/ cp ./DEBIAN build/deb/ -r mkdir dist -p - rm dist/input-remapper-1.6.0-beta.deb || true - dpkg-deb -Z gzip -b build/deb dist/input-remapper-1.6.0-beta.deb + rm dist/input-remapper-2.0.0-rc.deb || true + dpkg-deb -Z gzip -b build/deb dist/input-remapper-2.0.0-rc.deb } build_deb & diff --git a/setup.py b/setup.py index ff82c329..8fc2e96f 100644 --- a/setup.py +++ b/setup.py @@ -102,7 +102,7 @@ for po_file in glob.glob(PO_FILES): setup( name="input-remapper", - version="1.6.0-beta", + version="2.0.0-rc", description="A tool to change the mapping of your input device buttons", author="Sezanzeb", author_email="proxima@sezanzeb.de", diff --git a/tests/unit/test_migrations.py b/tests/unit/test_migrations.py index b986d956..fa56abbb 100644 --- a/tests/unit/test_migrations.py +++ b/tests/unit/test_migrations.py @@ -35,14 +35,21 @@ from evdev.ecodes import ( REL_Y, REL_WHEEL_HI_RES, REL_HWHEEL_HI_RES, + KEY_A, ) from inputremapper.configs.mapping import UIMapping from inputremapper.configs.migrations import migrate, config_version from inputremapper.configs.preset import Preset from inputremapper.configs.global_config import global_config -from inputremapper.configs.paths import touch, CONFIG_PATH, mkdir, get_preset_path -from inputremapper.logger import IS_BETA +from inputremapper.configs.paths import ( + touch, + CONFIG_PATH, + mkdir, + get_preset_path, + get_config_path, + remove, +) from inputremapper.configs.input_config import InputCombination, InputConfig from inputremapper.user import HOME @@ -50,6 +57,20 @@ from inputremapper.logger import VERSION class TestMigrations(unittest.TestCase): + def setUp(self): + # some extra care to ensure those tests are not destroying actual presets + self.assertTrue(HOME.startswith("/tmp")) + self.assertTrue(CONFIG_PATH.startswith("/tmp")) + self.assertTrue(get_preset_path().startswith("/tmp")) + self.assertTrue(get_preset_path("foo", "bar").startswith("/tmp")) + self.assertTrue(get_config_path().startswith("/tmp")) + self.assertTrue(get_config_path("foo").startswith("/tmp")) + + self.v1_dir = os.path.join(HOME, ".config", "input-remapper") + self.beta_dir = os.path.join( + HOME, ".config", "input-remapper", "beta_1.6.0-beta" + ) + def tearDown(self): quick_cleanup() self.assertEqual(len(global_config.iterate_autoload_presets()), 0) @@ -73,10 +94,7 @@ class TestMigrations(unittest.TestCase): def test_rename_config(self): old = os.path.join(HOME, ".config", "key-mapper") - if IS_BETA: - new = os.path.join(*os.path.split(CONFIG_PATH)[:-1]) - else: - new = CONFIG_PATH + new = CONFIG_PATH # we are not destroying our actual config files with this test self.assertTrue(new.startswith(tmp), f'Expected "{new}" to start with "{tmp}"') @@ -553,6 +571,136 @@ class TestMigrations(unittest.TestCase): ), ) + def _create_v1_setup(self): + """Create all files needed to mimic an outdated v1 configuration.""" + device_name = "device_name" + + mkdir(os.path.join(self.v1_dir, "presets", device_name)) + v1_config = {"autoload": {device_name: "foo"}, "version": "1.0"} + with open(os.path.join(self.v1_dir, "config.json"), "w") as file: + json.dump(v1_config, file) + # insert something outdated that will be migrated, to ensure the files are + # first copied and then migrated. + with open( + os.path.join(self.v1_dir, "presets", device_name, "foo.json"), "w" + ) as file: + json.dump({"mapping": {f"{EV_KEY},1": "a"}}, file) + + def _create_beta_setup(self): + """Create all files needed to mimic a beta configuration.""" + device_name = "device_name" + + # same here, but a different contents to tell the difference + mkdir(os.path.join(self.beta_dir, "presets", device_name)) + beta_config = {"autoload": {device_name: "bar"}, "version": "1.6"} + with open(os.path.join(self.beta_dir, "config.json"), "w") as file: + json.dump(beta_config, file) + with open( + os.path.join(self.beta_dir, "presets", device_name, "bar.json"), "w" + ) as file: + json.dump( + [ + { + "input_combination": [ + {"type": EV_KEY, "code": 1}, + ], + "target_uinput": "keyboard", + "output_symbol": "b", + "mapping_type": "key_macro", + } + ], + file, + ) + + def test_prioritize_v1_over_beta_configs(self): + # if both v1 and beta presets and config exist, migrate v1 + remove(get_config_path()) + + device_name = "device_name" + self._create_v1_setup() + self._create_beta_setup() + + self.assertFalse(os.path.exists(get_preset_path(device_name, "foo"))) + self.assertFalse(os.path.exists(get_config_path("config.json"))) + + migrate() + + self.assertTrue(os.path.exists(get_preset_path(device_name, "foo"))) + self.assertTrue(os.path.exists(get_config_path("config.json"))) + self.assertFalse(os.path.exists(get_preset_path(device_name, "bar"))) + + # expect all original files to still exist + self.assertTrue(os.path.join(self.v1_dir, "config.json")) + self.assertTrue(os.path.join(self.v1_dir, "presets", "foo.json")) + self.assertTrue(os.path.join(self.beta_dir, "config.json")) + self.assertTrue(os.path.join(self.beta_dir, "presets", "bar.json")) + + # v1 configs should be in the v2 dir now, and migrated + with open(get_config_path("config.json"), "r") as f: + config_json = json.load(f) + self.assertDictEqual( + config_json, {"autoload": {device_name: "foo"}, "version": VERSION} + ) + with open(get_preset_path(device_name, "foo.json"), "r") as f: + os.system(f'cat { get_preset_path(device_name, "foo.json") }') + preset_foo_json = json.load(f) + self.assertEqual( + preset_foo_json, + [ + { + "input_combination": [ + {"type": EV_KEY, "code": 1}, + ], + "target_uinput": "keyboard", + "output_symbol": "a", + "mapping_type": "key_macro", + } + ], + ) + + def test_copy_over_beta_configs(self): + # same as test_prioritize_v1_over_beta_configs, but only create the beta + # directory without any v1 presets. + remove(get_config_path()) + + device_name = "device_name" + self._create_beta_setup() + + self.assertFalse(os.path.exists(get_preset_path(device_name, "bar"))) + self.assertFalse(os.path.exists(get_config_path("config.json"))) + + migrate() + + self.assertTrue(os.path.exists(get_preset_path(device_name, "bar"))) + self.assertTrue(os.path.exists(get_config_path("config.json"))) + + # expect all original files to still exist + self.assertTrue(os.path.join(self.beta_dir, "config.json")) + self.assertTrue(os.path.join(self.beta_dir, "presets", "bar.json")) + + # beta configs should be in the v2 dir now + with open(get_config_path("config.json"), "r") as f: + config_json = json.load(f) + self.assertDictEqual( + config_json, {"autoload": {device_name: "bar"}, "version": VERSION} + ) + with open(get_preset_path(device_name, "bar.json"), "r") as f: + os.system(f'cat { get_preset_path(device_name, "bar.json") }') + preset_foo_json = json.load(f) + self.assertEqual( + preset_foo_json, + [ + { + "input_combination": [ + {"type": EV_KEY, "code": 1}, + ], + "target_uinput": "keyboard", + "output_symbol": "b", + "mapping_type": "key_macro", + } + ], + ) + if __name__ == "__main__": unittest.main()