diff --git a/.docker-test b/.docker-test
new file mode 100644
index 0000000..d36fad8
--- /dev/null
+++ b/.docker-test
@@ -0,0 +1,32 @@
+#!/bin/bash
+
+ruby_version=${1:-2.4.2}
+
+if ! rvm use ruby-${ruby_version} &>/dev/null ; then
+  echo "The ruby version '${ruby_version}' doesn't exist!"
+  echo "Available versions are:"
+  rvm list rubies strings | cut -d '-' -f2
+  exit 2
+fi
+
+echo '# ---------------------------------'
+echo "# Use ruby version: ${ruby_version}"
+echo '# ---------------------------------'
+
+cp -r /mpw ~/mpw
+cd ~/mpw
+gem install bundler --no-ri --no-rdoc
+bundle install
+gem build mpw.gemspec
+gem install mpw-$(cat VERSION).gem
+cp -a /dev/urandom /dev/random
+
+rubocop
+ruby ./test/init.rb
+ruby ./test/test_config.rb
+ruby ./test/test_item.rb
+ruby ./test/test_mpw.rb
+ruby ./test/test_translate.rb
+ruby ./test/init.rb
+ruby ./test/test_cli.rb
+ruby ./test/test_import.rb
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..afd83c3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+Gemfile.lock
+*.gem
+.yardoc
+doc
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..56d1540
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,38 @@
+
+AllCops:
+  Exclude:
+    - db/**/*
+    - config/**/*
+    - Vagrantfile
+  TargetRubyVersion: 2.3
+
+Naming/AccessorMethodName:
+  Enabled: false
+
+Lint/RescueWithoutErrorClass:
+  Enabled: false
+
+Metrics/LineLength:
+  Max: 120
+Metrics/CyclomaticComplexity:
+  Enabled: false
+Metrics/PerceivedComplexity:
+  Enabled: false
+Metrics/MethodLength:
+  Enabled: false
+Metrics/BlockLength:
+  Enabled: false
+Metrics/ClassLength:
+  Enabled: false
+Metrics/AbcSize:
+  Enabled: false
+
+Style/NumericLiteralPrefix:
+  Enabled: false
+Style/FrozenStringLiteralComment:
+  Enabled: false
+Style/CommandLiteral:
+  Enabled: true
+  EnforcedStyle: percent_x
+Style/Documentation:
+  Enabled: false
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..14d696a
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,23 @@
+language: ruby
+dist: precise
+rvm:
+  - 2.4.2
+  - 2.3.5
+  - 2.2.8
+  - 2.1.10
+install:
+  - sudo cp -a /dev/urandom /dev/random
+  - sudo apt-get purge -y gnupg-agent gnupg2
+  - bundle install
+  - gem build mpw.gemspec
+  - gem install mpw-$(cat VERSION).gem
+script:
+  - rubocop
+  - ruby ./test/init.rb
+  - ruby ./test/test_config.rb
+  - ruby ./test/test_item.rb
+  - ruby ./test/test_mpw.rb
+  - ruby ./test/test_translate.rb
+  - ruby ./test/init.rb
+  - ruby ./test/test_cli.rb
+  - ruby ./test/test_import.rb
diff --git a/CHANGELOG b/CHANGELOG
deleted file mode 100644
index 21cf7ae..0000000
--- a/CHANGELOG
+++ /dev/null
@@ -1,7 +0,0 @@
-= CHANGELOG =
-
-== v1.1.0 ==
-
-* Add sync with MPW Server
-* Add MPW Server
-* Fix minors bugs
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..199b863
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,98 @@
+# CHANGELOG
+## v4.2.2 (2017-08-15)
+
+  * minor improvements in the interface
+
+## v4.2.1 (2017-07-30)
+
+  * fix bug in otp generator
+
+## v4.2.0 (2017-06-06)
+
+ * feat: improve the interface
+ * feat: add copy url
+ * feat: add unit tests for cli
+ * feat: comment the code with yarn syntax
+ * fix several bugs
+ * fix translations
+
+## v4.1.1 (2017-05-03)
+
+ * fix bug in init
+
+## v4.1.0 (2017-04-22)
+
+ * feat: add options to update or add an item in command line
+ * feat: print config
+ * feat: add a specific path for a wallet
+ * feat: add rubocop to fix syntax
+ * fix: pinentry mode with gpg >= 2.1
+ * remove SSH and FTP synchronization
+
+## v4.0.0 (2017-03-09)
+
+ * feature: set default wallet
+ * add option for generate a random password when you update an item
+ * fix encryption when you share an existing wallet
+ * several bugs fix
+
+## v4.0.0-beta1 (2017-02-16)
+
+ * add manage share key with new interface
+
+## v4.0.0-beta (2016-11-11)
+
+ * new interface with a table
+ * new command line interface
+ * use text editor for add or update an item
+ * fix generate gpg key with RSA
+ * several bugs fix
+ * add unit tests
+
+## v3.2.1 (2016-08-06)
+
+ * fix bug when add a new item
+
+## v3.2.0 (2016-08-03)
+
+ * add support OTP
+ * fix bug in synchronize
+ * improve interface
+
+## v3.1.0 (2016-07-09)
+
+ * add clipboard
+ * can change gpg version
+ * minor change in interface
+ * several bugs fix
+
+## v3.0.0 (2016-07-05)
+
+ * new storage format
+ * new share system
+ * remove MPW server
+
+## v2.0.3 (2015-09-27)
+
+ * add no-sync option
+
+## v2.0.1 (2015-06-23)
+
+ * fix mpw-ssh
+
+## v2.0.0 (2015-06-22)
+
+ * change format csv to yaml
+ * easy install with gem
+ * add sync with ftp and ssh
+ * many improvement
+
+## v1.1.0 (2014-01-28)
+
+ * Add sync with MPW Server
+ * Add MPW Server
+ * Fix minors bugs
+
+## v1.0.0 (2014-01-15)
+
+  * first release
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..6654780
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,16 @@
+FROM debian:stretch
+MAINTAINER Adrien Waksberg "mpw@yae.im"
+
+RUN apt update
+RUN apt dist-upgrade -y
+
+RUN apt install -y procps gnupg1 curl git
+RUN ln -snvf /usr/bin/gpg1 /usr/bin/gpg
+RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 7D2BAF1CF37B13E2069D6956105BD0E739499BDB
+RUN curl -sSL https://get.rvm.io | bash -s stable
+RUN echo 'source "/usr/local/rvm/scripts/rvm"' >> /etc/bash.bashrc
+
+RUN /bin/bash -l -c "rvm install 2.4.2"
+RUN /bin/bash -l -c "rvm install 2.3.5"
+RUN /bin/bash -l -c "rvm install 2.2.8"
+RUN /bin/bash -l -c "rvm install 2.1.10"
diff --git a/Gemfile b/Gemfile
new file mode 100644
index 0000000..a76b2c1
--- /dev/null
+++ b/Gemfile
@@ -0,0 +1,14 @@
+source 'https://rubygems.org'
+gem 'clipboard', '~> 1.1', '>= 1.1.1'
+gem 'colorize',  '~> 0.8', '>= 0.8.1'
+gem 'gpgme',     '~> 2.0', '>= 2.0.14'
+gem 'highline',  '~> 1.7', '>= 1.7.8'
+gem 'i18n',      '~> 0.9', '>= 0.9.1'
+gem 'locale',    '~> 2.1', '>= 2.1.2'
+gem 'rotp',      '~> 3.3', '>= 3.3.0'
+
+group :development do
+  gem 'rubocop', '0.50.0'
+  gem 'test-unit'
+  gem 'yard'
+end
diff --git a/LICENSE b/LICENSE
index d159169..0c5ea8e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,339 +1,201 @@
-                    GNU GENERAL PUBLIC LICENSE
-                       Version 2, June 1991
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
 
- Copyright (C) 1989, 1991 Free Software Foundation, Inc.,
- 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 
-                            Preamble
+   1. Definitions.
 
-  The licenses for most software are designed to take away your
-freedom to share and change it.  By contrast, the GNU General Public
-License is intended to guarantee your freedom to share and change free
-software--to make sure the software is free for all its users.  This
-General Public License applies to most of the Free Software
-Foundation's software and to any other program whose authors commit to
-using it.  (Some other Free Software Foundation software is covered by
-the GNU Lesser General Public License instead.)  You can apply it to
-your programs, too.
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
 
-  When we speak of free software, we are referring to freedom, not
-price.  Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-this service if you wish), that you receive source code or can get it
-if you want it, that you can change the software or use pieces of it
-in new free programs; and that you know you can do these things.
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
 
-  To protect your rights, we need to make restrictions that forbid
-anyone to deny you these rights or to ask you to surrender the rights.
-These restrictions translate to certain responsibilities for you if you
-distribute copies of the software, or if you modify it.
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
 
-  For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must give the recipients all the rights that
-you have.  You must make sure that they, too, receive or can get the
-source code.  And you must show them these terms so they know their
-rights.
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
 
-  We protect your rights with two steps: (1) copyright the software, and
-(2) offer you this license which gives you legal permission to copy,
-distribute and/or modify the software.
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
 
-  Also, for each author's protection and ours, we want to make certain
-that everyone understands that there is no warranty for this free
-software.  If the software is modified by someone else and passed on, we
-want its recipients to know that what they have is not the original, so
-that any problems introduced by others will not reflect on the original
-authors' reputations.
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
 
-  Finally, any free program is threatened constantly by software
-patents.  We wish to avoid the danger that redistributors of a free
-program will individually obtain patent licenses, in effect making the
-program proprietary.  To prevent this, we have made it clear that any
-patent must be licensed for everyone's free use or not licensed at all.
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
 
-  The precise terms and conditions for copying, distribution and
-modification follow.
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
 
-                    GNU GENERAL PUBLIC LICENSE
-   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
 
-  0. This License applies to any program or other work which contains
-a notice placed by the copyright holder saying it may be distributed
-under the terms of this General Public License.  The "Program", below,
-refers to any such program or work, and a "work based on the Program"
-means either the Program or any derivative work under copyright law:
-that is to say, a work containing the Program or a portion of it,
-either verbatim or with modifications and/or translated into another
-language.  (Hereinafter, translation is included without limitation in
-the term "modification".)  Each licensee is addressed as "you".
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
 
-Activities other than copying, distribution and modification are not
-covered by this License; they are outside its scope.  The act of
-running the Program is not restricted, and the output from the Program
-is covered only if its contents constitute a work based on the
-Program (independent of having been made by running the Program).
-Whether that is true depends on what the Program does.
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
 
-  1. You may copy and distribute verbatim copies of the Program's
-source code as you receive it, in any medium, provided that you
-conspicuously and appropriately publish on each copy an appropriate
-copyright notice and disclaimer of warranty; keep intact all the
-notices that refer to this License and to the absence of any warranty;
-and give any other recipients of the Program a copy of this License
-along with the Program.
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
 
-You may charge a fee for the physical act of transferring a copy, and
-you may at your option offer warranty protection in exchange for a fee.
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
 
-  2. You may modify your copy or copies of the Program or any portion
-of it, thus forming a work based on the Program, and copy and
-distribute such modifications or work under the terms of Section 1
-above, provided that you also meet all of these conditions:
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
 
-    a) You must cause the modified files to carry prominent notices
-    stating that you changed the files and the date of any change.
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
 
-    b) You must cause any work that you distribute or publish, that in
-    whole or in part contains or is derived from the Program or any
-    part thereof, to be licensed as a whole at no charge to all third
-    parties under the terms of this License.
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
 
-    c) If the modified program normally reads commands interactively
-    when run, you must cause it, when started running for such
-    interactive use in the most ordinary way, to print or display an
-    announcement including an appropriate copyright notice and a
-    notice that there is no warranty (or else, saying that you provide
-    a warranty) and that users may redistribute the program under
-    these conditions, and telling the user how to view a copy of this
-    License.  (Exception: if the Program itself is interactive but
-    does not normally print such an announcement, your work based on
-    the Program is not required to print an announcement.)
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
 
-These requirements apply to the modified work as a whole.  If
-identifiable sections of that work are not derived from the Program,
-and can be reasonably considered independent and separate works in
-themselves, then this License, and its terms, do not apply to those
-sections when you distribute them as separate works.  But when you
-distribute the same sections as part of a whole which is a work based
-on the Program, the distribution of the whole must be on the terms of
-this License, whose permissions for other licensees extend to the
-entire whole, and thus to each and every part regardless of who wrote it.
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
 
-Thus, it is not the intent of this section to claim rights or contest
-your rights to work written entirely by you; rather, the intent is to
-exercise the right to control the distribution of derivative or
-collective works based on the Program.
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
 
-In addition, mere aggregation of another work not based on the Program
-with the Program (or with a work based on the Program) on a volume of
-a storage or distribution medium does not bring the other work under
-the scope of this License.
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
 
-  3. You may copy and distribute the Program (or a work based on it,
-under Section 2) in object code or executable form under the terms of
-Sections 1 and 2 above provided that you also do one of the following:
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
 
-    a) Accompany it with the complete corresponding machine-readable
-    source code, which must be distributed under the terms of Sections
-    1 and 2 above on a medium customarily used for software interchange; or,
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
 
-    b) Accompany it with a written offer, valid for at least three
-    years, to give any third party, for a charge no more than your
-    cost of physically performing source distribution, a complete
-    machine-readable copy of the corresponding source code, to be
-    distributed under the terms of Sections 1 and 2 above on a medium
-    customarily used for software interchange; or,
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
 
-    c) Accompany it with the information you received as to the offer
-    to distribute corresponding source code.  (This alternative is
-    allowed only for noncommercial distribution and only if you
-    received the program in object code or executable form with such
-    an offer, in accord with Subsection b above.)
+   END OF TERMS AND CONDITIONS
 
-The source code for a work means the preferred form of the work for
-making modifications to it.  For an executable work, complete source
-code means all the source code for all modules it contains, plus any
-associated interface definition files, plus the scripts used to
-control compilation and installation of the executable.  However, as a
-special exception, the source code distributed need not include
-anything that is normally distributed (in either source or binary
-form) with the major components (compiler, kernel, and so on) of the
-operating system on which the executable runs, unless that component
-itself accompanies the executable.
+   APPENDIX: How to apply the Apache License to your work.
 
-If distribution of executable or object code is made by offering
-access to copy from a designated place, then offering equivalent
-access to copy the source code from the same place counts as
-distribution of the source code, even though third parties are not
-compelled to copy the source along with the object code.
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "{}"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
 
-  4. You may not copy, modify, sublicense, or distribute the Program
-except as expressly provided under this License.  Any attempt
-otherwise to copy, modify, sublicense or distribute the Program is
-void, and will automatically terminate your rights under this License.
-However, parties who have received copies, or rights, from you under
-this License will not have their licenses terminated so long as such
-parties remain in full compliance.
+   Copyright 2017 Adrien Waksberg
 
-  5. You are not required to accept this License, since you have not
-signed it.  However, nothing else grants you permission to modify or
-distribute the Program or its derivative works.  These actions are
-prohibited by law if you do not accept this License.  Therefore, by
-modifying or distributing the Program (or any work based on the
-Program), you indicate your acceptance of this License to do so, and
-all its terms and conditions for copying, distributing or modifying
-the Program or works based on it.
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
 
-  6. Each time you redistribute the Program (or any work based on the
-Program), the recipient automatically receives a license from the
-original licensor to copy, distribute or modify the Program subject to
-these terms and conditions.  You may not impose any further
-restrictions on the recipients' exercise of the rights granted herein.
-You are not responsible for enforcing compliance by third parties to
-this License.
+       http://www.apache.org/licenses/LICENSE-2.0
 
-  7. If, as a consequence of a court judgment or allegation of patent
-infringement or for any other reason (not limited to patent issues),
-conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License.  If you cannot
-distribute so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you
-may not distribute the Program at all.  For example, if a patent
-license would not permit royalty-free redistribution of the Program by
-all those who receive copies directly or indirectly through you, then
-the only way you could satisfy both it and this License would be to
-refrain entirely from distribution of the Program.
-
-If any portion of this section is held invalid or unenforceable under
-any particular circumstance, the balance of the section is intended to
-apply and the section as a whole is intended to apply in other
-circumstances.
-
-It is not the purpose of this section to induce you to infringe any
-patents or other property right claims or to contest validity of any
-such claims; this section has the sole purpose of protecting the
-integrity of the free software distribution system, which is
-implemented by public license practices.  Many people have made
-generous contributions to the wide range of software distributed
-through that system in reliance on consistent application of that
-system; it is up to the author/donor to decide if he or she is willing
-to distribute software through any other system and a licensee cannot
-impose that choice.
-
-This section is intended to make thoroughly clear what is believed to
-be a consequence of the rest of this License.
-
-  8. If the distribution and/or use of the Program is restricted in
-certain countries either by patents or by copyrighted interfaces, the
-original copyright holder who places the Program under this License
-may add an explicit geographical distribution limitation excluding
-those countries, so that distribution is permitted only in or among
-countries not thus excluded.  In such case, this License incorporates
-the limitation as if written in the body of this License.
-
-  9. The Free Software Foundation may publish revised and/or new versions
-of the General Public License from time to time.  Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
-Each version is given a distinguishing version number.  If the Program
-specifies a version number of this License which applies to it and "any
-later version", you have the option of following the terms and conditions
-either of that version or of any later version published by the Free
-Software Foundation.  If the Program does not specify a version number of
-this License, you may choose any version ever published by the Free Software
-Foundation.
-
-  10. If you wish to incorporate parts of the Program into other free
-programs whose distribution conditions are different, write to the author
-to ask for permission.  For software which is copyrighted by the Free
-Software Foundation, write to the Free Software Foundation; we sometimes
-make exceptions for this.  Our decision will be guided by the two goals
-of preserving the free status of all derivatives of our free software and
-of promoting the sharing and reuse of software generally.
-
-                            NO WARRANTY
-
-  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
-FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
-OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
-PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
-OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
-MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
-TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
-PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
-REPAIR OR CORRECTION.
-
-  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
-REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
-INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
-OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
-TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
-YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
-PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
-POSSIBILITY OF SUCH DAMAGES.
-
-                     END OF TERMS AND CONDITIONS
-
-            How to Apply These Terms to Your New Programs
-
-  If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
-  To do so, attach the following notices to the program.  It is safest
-to attach them to the start of each source file to most effectively
-convey the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-    <one line to give the program's name and a brief idea of what it does.>
-    Copyright (C) <year>  <name of author>
-
-    This program is free software; you can redistribute it and/or modify
-    it under the terms of the GNU General Public License as published by
-    the Free Software Foundation; either version 2 of the License, or
-    (at your option) any later version.
-
-    This program is distributed in the hope that it will be useful,
-    but WITHOUT ANY WARRANTY; without even the implied warranty of
-    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-    GNU General Public License for more details.
-
-    You should have received a copy of the GNU General Public License along
-    with this program; if not, write to the Free Software Foundation, Inc.,
-    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
-
-Also add information on how to contact you by electronic and paper mail.
-
-If the program is interactive, make it output a short notice like this
-when it starts in an interactive mode:
-
-    Gnomovision version 69, Copyright (C) year name of author
-    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
-    This is free software, and you are welcome to redistribute it
-    under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License.  Of course, the commands you use may
-be called something other than `show w' and `show c'; they could even be
-mouse-clicks or menu items--whatever suits your program.
-
-You should also get your employer (if you work as a programmer) or your
-school, if any, to sign a "copyright disclaimer" for the program, if
-necessary.  Here is a sample; alter the names:
-
-  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
-  `Gnomovision' (which makes passes at compilers) written by James Hacker.
-
-  <signature of Ty Coon>, 1 April 1989
-  Ty Coon, President of Vice
-
-This General Public License does not permit incorporating your program into
-proprietary programs.  If your program is a subroutine library, you may
-consider it more useful to permit linking proprietary applications with the
-library.  If this is what you want to do, use the GNU Lesser General
-Public License instead of this License.
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/README.md b/README.md
index 277d937..be9e7cb 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,195 @@
-# Manage your passwords!
+# MPW: Manage your passwords!
+[![Version](https://img.shields.io/badge/latest_version-4.2.2-green.svg)](https://github.com/nishiki/manage-password/releases)
+[![Build Status](https://travis-ci.org/nishiki/manage-password.svg?branch=master)](https://travis-ci.org/nishiki/manage-password)
+[![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](https://github.com/nishiki/manage-password/blob/master/LICENSE)
 
-MPW is a little software which stores your passwords in an GPG encrypted file.
-MPW can sync your password with a MPW Server.
+mpw is a little software which stores your passwords in [GnuPG](http://www.gnupg.org/) encrypted files.
 
-# Installation
+## Features
 
-You must generate a GPG Key with GPG or with Seahorse (GUI on linux).
-This program work with ruby >= 1.9
+ * generate random password
+ * generate OTP code
+ * copy your login, password or otp in clipboard
+ * manage many wallets
+ * share a wallet with others GPG keys
 
-## On Debian/Ubuntu:
+## Install
 
-* apt-get install ruby ruby-gpgme ruby-highline ruby-i18n ruby-locale
+On debian or ubuntu:
+```
+apt install ruby ruby-dev xclip
+gem install mpw
+```
 
-For mpw-ssh:
-* apt-get install sshpass
+## How to use
+### First steps
+
+Initialize your first wallet:
+```
+mpw config --init user@host.com
+```
+
+Add your first item:
+```
+mpw add --host assurance.com --port 443 --user user_2132 --protocol https --random
+mpw add --host fric.com --user 230403 --otp-code 23434113 --protocol https --comment 'I love my bank' --random
+```
+
+And list your items:
+```
+mpw list
+```
+or search an item with
+```
+mpw list --pattern love
+mpw list --group bank
+```
+
+Output:
+```
+Assurance
+ ==========================================================================
+  ID | Host                        | User        | OTP | Comment          
+ ==========================================================================
+  1  | https://assurance.com:443   | user_2132   |     |                  
+
+Bank
+ ==========================================================================
+  ID | Host                        | User        | OTP | Comment          
+ ==========================================================================
+  3  | https://fric.com            | 230403      |  X  | I love my bank   
+```
+
+Copy a password, login or OTP code:
+```
+mpw copy -p assurance.com
+```
+
+Update an item:
+```
+mpw update -p assurance.com
+```
+
+Delete an item:
+```
+mpw delete -p assurance.com
+```
+
+### Manage wallets
+
+List all available wallets:
+```
+mpw wallet
+```
+
+List all GPG keys in wallet:
+```
+mpw wallet --list-keys [--wallet NAME]
+```
+
+Share with an other GPG key:
+```
+mpw wallet --add-gpg-key test42@localhost.com
+ or
+mpw wallet --add-gpg-key /path/to/file
+```
+
+Remove a GPG key:
+```
+mpw wallet --delete-gpg-key test42@localhost.com
+```
+
+### Export and import data
+
+You can export your data in yaml file with your passwords in clear text:
+```
+mpw export --file export.yml
+```
+
+Import data from an yaml file:
+```
+mpw import --file import.yml
+```
+
+Example yaml file for mpw:
+
+```
+---
+1:
+  host: fric.com
+  user: 230403
+  group: Bank
+  password: 5XdiTQOubRDw9B0aJoMlcEyL
+  protocol: https
+  port:
+  otp_key: 330223432
+  comment: I love my bank
+2:
+  host: assurance.com
+  user: user_2132
+  group: Assurance
+  password: DMyK6B3v4bWO52VzU7aTHIem
+  protocol: https
+  port: 443
+  otp_key:
+  comment:
+```
+
+### Config
+
+Print the current config
+```
+mpw config
+```
+
+Output:
+
+```
+Configuration
+ ==============================================
+  lang             | fr
+  gpg_key          | mpw@yae.im
+  default_wallet   |
+  config_dir       | /home/mpw/.config/mpw
+  pinmode          | true
+  gpg_exe          |
+  path_wallet_test | /tmp/test.mpw
+  password_numeric | true
+  password_alpha   | true
+  password_special | false
+  password_length  | 16
+
+```
+
+## Development
+
+Don't run the tests on your local machine, you risk to lost your datas.
+
+### Test on local machine with docker
+
+  * install [docker](https://docs.docker.com/engine/installation/)
+  * run the tests
+
+```
+docker run -v $(pwd):/mpw:ro -it nishiki/ruby:stretch /bin/bash -l /mpw/.docker-test
+```
+
+## License
+
+```
+* Author:: Adrien Waksberg <mpw@yae.im>
+
+Copyright (c) 2013-2017 Adrien Waksberg
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+```
diff --git a/VERSION b/VERSION
index e532f18..af8c8ec 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.1.0 - stable
+4.2.2
diff --git a/bin/mpw b/bin/mpw
new file mode 100755
index 0000000..11bc726
--- /dev/null
+++ b/bin/mpw
@@ -0,0 +1,72 @@
+#!/usr/bin/env ruby
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+$LOAD_PATH << File.expand_path('../../lib', __FILE__)
+
+require 'locale'
+require 'set'
+require 'i18n'
+require 'colorize'
+
+# --------------------------------------------------------- #
+# Set local
+# --------------------------------------------------------- #
+
+lang = Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1]
+
+if defined?(I18n.enforce_available_locales)
+  I18n.enforce_available_locales = true
+end
+
+I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
+I18n.load_path      = Dir["#{File.expand_path('../../i18n', __FILE__)}/*.yml"]
+I18n.default_locale = :en
+I18n.locale         = lang.to_sym
+
+# --------------------------------------------------------- #
+# Options
+# --------------------------------------------------------- #
+
+bin_dir = File.dirname(__FILE__)
+command = "#{bin_dir}/mpw-#{ARGV[0]}"
+
+if Dir.glob("#{bin_dir}/mpw-*").include?(command.to_s)
+  begin
+    Kernel.load(command)
+  rescue OptionParser::ParseError => e
+    puts "#{I18n.t('display.error')}: #{e}".red
+  end
+else
+  puts "#{I18n.t('option.usage')}: mpw COMMAND [options]\n\n"
+  puts 'Commands:'
+  puts "  add       #{I18n.t('command.add')}"
+  puts "  config    #{I18n.t('command.config')}"
+  puts "  copy      #{I18n.t('command.copy')}"
+  puts "  delete    #{I18n.t('command.delete')}"
+  puts "  export    #{I18n.t('command.export')}"
+  puts "  genpwd    #{I18n.t('command.genpwd')}"
+  puts "  import    #{I18n.t('command.import')}"
+  puts "  list      #{I18n.t('command.list')}"
+  puts "  update    #{I18n.t('command.update')}"
+  puts "  wallet    #{I18n.t('command.wallet')}"
+
+  exit 3
+end
diff --git a/bin/mpw-add b/bin/mpw-add
new file mode 100644
index 0000000..e08caae
--- /dev/null
+++ b/bin/mpw-add
@@ -0,0 +1,84 @@
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'optparse'
+require 'mpw/config'
+require 'mpw/cli'
+
+# --------------------------------------------------------- #
+# Options
+# --------------------------------------------------------- #
+
+values                = {}
+options               = {}
+options[:text_editor] = true
+
+OptionParser.new do |opts|
+  opts.banner = "#{I18n.t('option.usage')}: mpw add [options]"
+
+  opts.on('-c', '--config PATH', I18n.t('option.config')) do |config|
+    options[:config] = config
+  end
+
+  opts.on('-C', '--comment COMMENT', I18n.t('option.comment')) do |comment|
+    values[:comment] = comment
+  end
+
+  opts.on('-G', '--group NAME', I18n.t('option.new_group')) do |group|
+    values[:group] = group
+  end
+
+  opts.on('-h', '--help', I18n.t('option.help')) do
+    puts opts
+    exit 0
+  end
+
+  opts.on('-o', '--otp-code CODE', I18n.t('option.otp_code')) do |otp|
+    values[:otp_key] = otp
+  end
+
+  opts.on('-r', '--random', I18n.t('option.random_password')) do
+    options[:password] = true
+  end
+
+  opts.on('-t', '--text-editor', I18n.t('option.text_editor')) do
+    options[:text_editor] = true
+  end
+
+  opts.on('-u', '--url URL', I18n.t('option.url')) do |url|
+    values[:url] = url
+  end
+
+  opts.on('-U', '--user USER', I18n.t('option.user')) do |user|
+    values[:user] = user
+  end
+
+  opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet|
+    options[:wallet] = wallet
+  end
+end.parse!
+
+config = MPW::Config.new(options[:config])
+cli    = MPW::Cli.new(config)
+
+cli.load_config
+cli.get_wallet(options[:wallet])
+cli.decrypt
+cli.add(options[:password], options[:text_editor], values)
diff --git a/bin/mpw-config b/bin/mpw-config
new file mode 100644
index 0000000..2ac0f77
--- /dev/null
+++ b/bin/mpw-config
@@ -0,0 +1,121 @@
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'optparse'
+require 'mpw/config'
+require 'mpw/cli'
+
+# --------------------------------------------------------- #
+# Options
+# --------------------------------------------------------- #
+
+options = {}
+values  = {}
+
+OptionParser.new do |opts|
+  opts.banner = "#{I18n.t('option.usage')}: mpw config [options]"
+
+  opts.on('-c', '--config PATH', I18n.t('option.config')) do |config|
+    options[:config] = config
+  end
+
+  opts.on('-d', '--default-wallet NAME', I18n.t('option.default_wallet')) do |default_wallet|
+    values[:default_wallet] = default_wallet
+  end
+
+  opts.on('-g', '--gpg-exe PATH', I18n.t('option.gpg_exe')) do |gpg_exe|
+    values[:gpg_exe] = gpg_exe
+  end
+
+  opts.on('-h', '--help', I18n.t('option.help')) do
+    puts opts
+    exit 0
+  end
+
+  opts.on('-i', '--init GPG_KEY', I18n.t('option.init')) do |gpg_key|
+    options[:init]   = true
+    values[:gpg_key] = gpg_key
+  end
+
+  opts.on('-k', '--key GPG_KEY', I18n.t('option.gpg_key')) do |gpg_key|
+    values[:gpg_key] = gpg_key
+  end
+
+  opts.on('-L', '--lang LANG', I18n.t('option.lang')) do |lang|
+    values[:lang] = lang
+  end
+
+  opts.on('-P', '--enable-pinmode', I18n.t('option.pinmode')) do
+    values[:pinmode] = true
+  end
+
+  opts.on('-p', '--disable-pinmode', I18n.t('option.disable_pinmode')) do
+    values[:pinmode] = false
+  end
+
+  opts.on('-w', '--wallet-dir PATH', I18n.t('option.wallet_dir')) do |wallet_dir|
+    values[:wallet_dir] = wallet_dir
+  end
+
+  opts.on('-l', '--length NUMBER', I18n.t('option.length')) do |length|
+    values[:pwd_length] = length.to_i
+  end
+
+  opts.on('-n', '--numeric', I18n.t('option.numeric')) do
+    values[:pwd_numeric] = true
+  end
+
+  opts.on('-N', '--disable-numeric', I18n.t('option.disable_numeric')) do
+    values[:pwd_numeric] = false
+  end
+
+  opts.on('-s', '--special-chars', I18n.t('option.special_chars')) do
+    values[:pwd_special] = true
+  end
+
+  opts.on('-S', '--disable-special-chars', I18n.t('option.special_chars')) do
+    values[:pwd_special] = false
+  end
+
+  opts.on('-a', '--alpha', I18n.t('option.alpha')) do
+    values[:pwd_alpha] = true
+  end
+
+  opts.on('-A', '--disable-alpha', I18n.t('option.disable_alpha')) do
+    values[:pwd_alpha] = false
+  end
+end.parse!
+
+config = MPW::Config.new(options[:config])
+cli    = MPW::Cli.new(config)
+
+if options.key?(:init)
+  cli.setup(values)
+  cli.load_config
+  cli.get_wallet
+  cli.setup_gpg_key(values[:gpg_key])
+else
+  cli.load_config
+  if values.empty?
+    cli.list_config
+  else
+    cli.set_config(values)
+  end
+end
diff --git a/bin/mpw-copy b/bin/mpw-copy
new file mode 100644
index 0000000..fc3b6e0
--- /dev/null
+++ b/bin/mpw-copy
@@ -0,0 +1,68 @@
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'optparse'
+require 'mpw/config'
+require 'mpw/cli'
+
+# --------------------------------------------------------- #
+# Options
+# --------------------------------------------------------- #
+
+options             = {}
+options[:clipboard] = true
+values              = {}
+
+OptionParser.new do |opts|
+  opts.banner = "#{I18n.t('option.usage')}: mpw copy [options]"
+
+  opts.on('-c', '--config PATH', I18n.t('option.config')) do |config|
+    options[:config] = config
+  end
+
+  opts.on('-d', '--disable-clipboard', I18n.t('option.clipboard')) do
+    options[:clipboard] = false
+  end
+
+  opts.on('-g', '--group NAME', I18n.t('option.group')) do |group|
+    values[:group] = group
+  end
+
+  opts.on('-h', '--help', I18n.t('option.help')) do
+    puts opts
+    exit 0
+  end
+
+  opts.on('-p', '--pattern PATTERN', I18n.t('option.pattern')) do |pattern|
+    values[:pattern] = pattern
+  end
+
+  opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet|
+    options[:wallet] = wallet
+  end
+end.parse!
+
+config = MPW::Config.new(options[:config])
+cli    = MPW::Cli.new(config)
+
+cli.load_config
+cli.get_wallet(options[:wallet])
+cli.decrypt
+cli.copy(options[:clipboard], values)
diff --git a/bin/mpw-delete b/bin/mpw-delete
new file mode 100644
index 0000000..48eb792
--- /dev/null
+++ b/bin/mpw-delete
@@ -0,0 +1,63 @@
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'optparse'
+require 'mpw/config'
+require 'mpw/cli'
+
+# --------------------------------------------------------- #
+# Options
+# --------------------------------------------------------- #
+
+options = {}
+values  = {}
+
+OptionParser.new do |opts|
+  opts.banner = "#{I18n.t('option.usage')}: mpw delete [options]"
+
+  opts.on('-c', '--config PATH', I18n.t('option.config')) do |config|
+    options[:config] = config
+  end
+
+  opts.on('-g', '--group NAME', I18n.t('option.group')) do |group|
+    values[:group] = group
+  end
+
+  opts.on('-h', '--help', I18n.t('option.help')) do
+    puts opts
+    exit 0
+  end
+
+  opts.on('-p', '--pattern PATTERN', I18n.t('option.pattern')) do |pattern|
+    values[:pattern] = pattern
+  end
+
+  opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet|
+    options[:wallet] = wallet
+  end
+end.parse!
+
+config = MPW::Config.new(options[:config])
+cli    = MPW::Cli.new(config)
+
+cli.load_config
+cli.get_wallet(options[:wallet])
+cli.decrypt
+cli.delete(values)
diff --git a/bin/mpw-export b/bin/mpw-export
new file mode 100644
index 0000000..92eb7bd
--- /dev/null
+++ b/bin/mpw-export
@@ -0,0 +1,64 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'optparse'
+require 'mpw/config'
+require 'mpw/cli'
+
+# --------------------------------------------------------- #
+# Options
+# --------------------------------------------------------- #
+
+options = {}
+values  = {}
+
+OptionParser.new do |opts|
+  opts.banner = "#{I18n.t('option.usage')}: mpw wallet [options]"
+
+  opts.on('-c', '--config PATH', I18n.t('option.config')) do |config|
+    options[:config] = config
+  end
+
+  opts.on('-f', '--file PATH', I18n.t('option.file_export')) do |file|
+    options[:file] = file
+  end
+
+  opts.on('-g', '--group GROUP', I18n.t('option.group')) do |group|
+    values[:group] = group
+  end
+
+  opts.on('-h', '--help', I18n.t('option.help')) do
+    puts opts
+    exit 0
+  end
+
+  opts.on('-p', '--pattern PATTERN', I18n.t('option.pattern')) do |pattern|
+    values[:pattern] = pattern
+  end
+
+  opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet|
+    options[:wallet] = wallet
+  end
+end.parse!
+
+config = MPW::Config.new(options[:config])
+cli    = MPW::Cli.new(config)
+
+cli.load_config
+cli.get_wallet(options[:wallet])
+cli.decrypt
+cli.export(options[:file], values)
diff --git a/bin/mpw-genpwd b/bin/mpw-genpwd
new file mode 100644
index 0000000..f6ca795
--- /dev/null
+++ b/bin/mpw-genpwd
@@ -0,0 +1,49 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'optparse'
+require 'mpw/mpw'
+
+options = {}
+
+OptionParser.new do |opts|
+  opts.banner = "#{I18n.t('option.usage')}: mpw passwd [options]"
+
+  opts.on('-h', '--help', I18n.t('option.help')) do
+    puts opts
+    exit 0
+  end
+
+  opts.on('-l', '--length NUMBER', I18n.t('option.length')) do |length|
+    options[:length] = length.to_i
+  end
+
+  opts.on('-n', '--numeric', I18n.t('option.numeric')) do
+    options[:numeric] = true
+  end
+
+  opts.on('-s', '--special-chars', I18n.t('option.special_chars')) do
+    options[:special] = true
+  end
+
+  opts.on('-a', '--alpha', I18n.t('option.alpha')) do
+    options[:alpha] = true
+  end
+end.parse!
+
+puts MPW::MPW.password(options)
+exit 0
diff --git a/bin/mpw-import b/bin/mpw-import
new file mode 100644
index 0000000..d0deae9
--- /dev/null
+++ b/bin/mpw-import
@@ -0,0 +1,65 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'optparse'
+require 'mpw/config'
+require 'mpw/cli'
+
+# --------------------------------------------------------- #
+# Options
+# --------------------------------------------------------- #
+
+formats =
+  Dir["#{File.expand_path('../../lib/mpw/import', __FILE__)}/*.rb"]
+    .map { |v| File.basename(v, '.rb') }
+    .join(', ')
+options = {
+  format: 'mpw'
+}
+
+OptionParser.new do |opts|
+  opts.banner = "#{I18n.t('option.usage')}: mpw import [options]"
+
+  opts.on('-c', '--config PATH', I18n.t('option.config')) do |config|
+    options[:config] = config
+  end
+
+  opts.on('-f', '--file PATH', I18n.t('option.file_import')) do |file|
+    options[:file] = file
+  end
+
+  opts.on('-F', '--format STRING', I18n.t('option.file_format', formats: formats)) do |format|
+    options[:format] = format
+  end
+
+  opts.on('-h', '--help', I18n.t('option.help')) do
+    puts opts
+    exit 0
+  end
+
+  opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet|
+    options[:wallet] = wallet
+  end
+end.parse!
+
+config = MPW::Config.new(options[:config])
+cli    = MPW::Cli.new(config)
+
+cli.load_config
+cli.get_wallet(options[:wallet])
+cli.decrypt
+cli.import(options[:file], options[:format])
diff --git a/bin/mpw-list b/bin/mpw-list
new file mode 100644
index 0000000..fb7899c
--- /dev/null
+++ b/bin/mpw-list
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'optparse'
+require 'mpw/config'
+require 'mpw/cli'
+
+# --------------------------------------------------------- #
+# Options
+# --------------------------------------------------------- #
+
+options = {}
+values  = {}
+
+OptionParser.new do |opts|
+  opts.banner = "#{I18n.t('option.usage')}: mpw list [options]"
+
+  opts.on('-c', '--config PATH', I18n.t('option.config')) do |config|
+    options[:config] = config
+  end
+
+  opts.on('-g', '--group NAME', I18n.t('option.group')) do |group|
+    values[:group] = group
+  end
+
+  opts.on('-h', '--help', I18n.t('option.help')) do
+    puts opts
+    exit 0
+  end
+
+  opts.on('-p', '--pattern PATTERN', I18n.t('option.pattern')) do |pattern|
+    values[:pattern] = pattern
+  end
+
+  opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet|
+    options[:wallet] = wallet
+  end
+end.parse!
+
+config = MPW::Config.new(options[:config])
+cli    = MPW::Cli.new(config)
+
+cli.load_config
+cli.get_wallet(options[:wallet])
+cli.decrypt
+cli.list(values)
diff --git a/bin/mpw-update b/bin/mpw-update
new file mode 100644
index 0000000..26a55c9
--- /dev/null
+++ b/bin/mpw-update
@@ -0,0 +1,92 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'optparse'
+require 'mpw/config'
+require 'mpw/cli'
+
+# --------------------------------------------------------- #
+# Options
+# --------------------------------------------------------- #
+
+values                = {}
+search                = {}
+options               = {}
+options[:text_editor] = false
+
+OptionParser.new do |opts|
+  opts.banner = "#{I18n.t('option.usage')}: mpw update [options]"
+
+  opts.on('-c', '--config PATH', I18n.t('option.config')) do |config|
+    options[:config] = config
+  end
+
+  opts.on('-C', '--comment COMMENT', I18n.t('option.comment')) do |comment|
+    values[:comment] = comment
+  end
+
+  opts.on('-g', '--group NAME', I18n.t('option.group')) do |group|
+    search[:group] = group
+  end
+
+  opts.on('-G', '--new-group NAME', I18n.t('option.new_group')) do |group|
+    values[:group] = group
+  end
+
+  opts.on('-h', '--help', I18n.t('option.help')) do
+    puts opts
+    exit 0
+  end
+
+  opts.on('-o', '--otp-code CODE', I18n.t('option.otp_code')) do |otp|
+    values[:otp_key] = otp
+  end
+
+  opts.on('-p', '--pattern PATTERN', I18n.t('option.pattern')) do |pattern|
+    search[:pattern] = pattern
+  end
+
+  opts.on('-r', '--random', I18n.t('option.random_password')) do
+    options[:password] = true
+  end
+
+  opts.on('-t', '--text-editor', I18n.t('option.text_editor')) do
+    options[:text_editor] = true
+  end
+
+  opts.on('-u', '--url URL', I18n.t('option.url')) do |url|
+    values[:url] = url
+  end
+
+  opts.on('-U', '--user USER', I18n.t('option.user')) do |user|
+    values[:user] = user
+  end
+
+  opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet|
+    options[:wallet] = wallet
+  end
+end.parse!
+
+config = MPW::Config.new(options[:config])
+cli    = MPW::Cli.new(config)
+
+options[:text_editor] = true if values.empty?
+
+cli.load_config
+cli.get_wallet(options[:wallet])
+cli.decrypt
+cli.update(options[:password], options[:text_editor], search, values)
diff --git a/bin/mpw-wallet b/bin/mpw-wallet
new file mode 100644
index 0000000..6518283
--- /dev/null
+++ b/bin/mpw-wallet
@@ -0,0 +1,90 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'optparse'
+require 'mpw/config'
+require 'mpw/cli'
+
+# --------------------------------------------------------- #
+# Options
+# --------------------------------------------------------- #
+
+options          = {}
+options[:delete] = false
+
+OptionParser.new do |opts|
+  opts.banner = "#{I18n.t('option.usage')}: mpw wallet [options]"
+
+  opts.on('-a', '--add-gpg-key NAME', I18n.t('option.add_gpg_key')) do |gpg_key|
+    options[:gpg_key] = gpg_key
+  end
+
+  opts.on('-c', '--config PATH', I18n.t('option.config')) do |config|
+    options[:config] = config
+  end
+
+  opts.on('-d', '--delete-gpg-key NAME', I18n.t('option.delete_gpg_key')) do |gpg_key|
+    options[:gpg_key] = gpg_key
+    options[:delete]  = true
+  end
+
+  opts.on('-h', '--help', I18n.t('option.help')) do
+    puts opts
+    exit 0
+  end
+
+  opts.on('-l', '--list', I18n.t('option.list')) do
+    options[:list] = true
+  end
+
+  opts.on('-L', '--list-keys', I18n.t('option.list_keys')) do
+    options[:list_keys] = true
+  end
+
+  opts.on('-p', '--path PATH', I18n.t('option.path')) do |path|
+    options[:path] = path
+  end
+
+  opts.on('-P', '--default-path', I18n.t('option.default_path')) do
+    options[:path] = 'default'
+  end
+
+  opts.on('-w', '--wallet NAME', I18n.t('option.wallet')) do |wallet|
+    options[:wallet] = wallet
+  end
+end.parse!
+
+config = MPW::Config.new(options[:config])
+cli    = MPW::Cli.new(config)
+
+cli.load_config
+
+if options.key?(:path)
+  cli.get_wallet(options[:wallet])
+  cli.set_wallet_path(options[:path])
+elsif options.key?(:list_keys) || options.key?(:gpg_key)
+  cli.get_wallet(options[:wallet])
+  cli.decrypt
+
+  if options.key?(:list_keys)
+    cli.list_keys
+  elsif options.key?(:gpg_key)
+    options[:delete] ? cli.delete_key(options[:gpg_key]) : cli.add_key(options[:gpg_key])
+  end
+else
+  cli.list_wallet
+end
diff --git a/i18n/cli/en.yml b/i18n/cli/en.yml
deleted file mode 100644
index bf8b9fc..0000000
--- a/i18n/cli/en.yml
+++ /dev/null
@@ -1,125 +0,0 @@
----
-en:
-  error:
-    config:
-      write: "Can't write the config file!"
-      check: "Checkconfig failed!"
-      key_bad_format: "The key string isn't in good format!"
-    delete:
-      id_no_exist: "Can't delete the item %{id}, it doesn't exist!"
-    export:
-      write: "Can't export, unable to write in %{file}!"
-    gpg_file: 
-      decrypt: "Can't decrypt file!"
-      encrypt: "Can't encrypt the GPG file!"
-    import:
-      bad_format: "Can't import, the file is badly formated!"
-      read: "Can't import, unable to read %{file}!"
-    update:
-      name_empty: "You must define a name!"
-    sync: 
-      connection: "Connection fail!"
-      communication: "A communication problem with the server is appeared!"
-      no_data: "Nothing data!"
-      not_authorized: "You haven't the access to remote file!"
-      unknown: "An unknown error is occured!" 
-  option:
-    usage: "Usage"
-    show: "Search and show the items"
-    show_all: "Show all items"
-    update: "Update an item"
-    remove: "Delete an item"
-    group: "Search the items with specified group"
-    add: "Add an item"
-    config: "Specify the configuration file to use"
-    setup: "Create a new configuration file" 
-    protocol: "Select the items with the specified protocol"
-    export: "Export all items in a CSV file"
-    import: "Import item since a CSV file"
-    force: "Force an action"
-    format: "Change the display items format by an alternative format"
-    generate_password: "Generate a random password (default 8 characters)"
-    help: "Show this help message"
-  form:
-    add:
-      title: "Add a new item"
-      name: "Enter the name: "
-      group: "Enter the group (optional): "
-      server: "Enter the hostname or ip: "
-      protocol: "Enter the protocol of the connection (ssh, http, other): "
-      login: "Enter the login connection: "
-      password: "Enter the the password: "
-      port: "Enter the connection port (optional): "
-      comment: "Enter a comment (optional): "
-      valid: "Item has been added!"
-    delete:
-      ask: "Are you sure you want to remove the item %{id} ?"
-      valid: "The item %{id} has been removed!"
-      not_valid: "The item %{id} hasn't been removed, because it doesn't exist!"
-    import:
-      ask: "Are you sure you want to import this file %{file} ?"
-      valid: "The import is succesfull!"
-      not_valid: "No data to import!"
-    setup:
-      title: "Setup a new config file"
-      lang: "Choose your language (en, fr, ...): "
-      gpg_key: "Enter the GPG key: " 
-      gpg_file: "Enter the path to encrypt file [default=%{home}/.mpw.gpg]: "
-      timeout: "Enter the timeout (in seconde) to GPG password [default=60]: "
-      sync_host: "Synchronization server: "
-      sync_port: "Port of the synchronization server: "
-      sync_pwd: "Password for the synchronization: "
-      sync_suffix: "Suffix for the synchronization (optionnal): "
-      valid: "The config file has been created!"
-    update:
-      title: "Update an item"
-      name: "Enter the name [%{name}]: "
-      group: "Enter the group [%{group}]: "
-      server: "Enter the hostname or ip [%{server}]: "
-      protocol: "Enter the protocol of the connection [%{protocol}]: "
-      login: "Enter the login connection [%{login}]: "
-      password: "Enter the the password: "
-      port: "Enter the connection port [%{port}]: "
-      comment: "Enter a comment [%{comment}]: "
-      valid: "Item has been updated!" 
-  interactive:
-    ask_password: "Password GPG: "
-    bad_password: "Bad password!"
-    goodbye: "Goodbye!"
-    unknown_command: "Unknown command!"
-    option:
-      title: "Help"
-      show: "Search and show the results"
-      group: "Change the group for the search"
-      add: "Add an item"
-      update: "Update an item"
-      remove: "Remove an item"
-      help: "Show this message"
-      quit: "Quit the software"
-  display:
-    comment: "Comment"
-    error: "ERROR"
-    gpg_password: "Password GPG: "
-    group: "Group"
-    login: "Login"
-    name: "Name"
-    nothing: "Nothing result!"
-    password: "Password"
-    port: "Port"
-    protocol: "Protocol"
-    server: "Server"
-  ssh:
-    option:
-      usage: "Usage"
-      login: "Change the login"
-      server: "Change the host or the ip"
-      port: "Change the port"
-      help: "Show this help message"
-    display:
-      connect: "Connection to:"
-      nothing: "Nothing result!"
-  formats:
-    default: ! '%Y-%m-%d'
-    long: ! '%B %d, %Y'
-    short: ! '%b %d'
-    custom: ! '%A, %M %B, %Y @ %l:%M%P'
diff --git a/i18n/cli/fr.yml b/i18n/cli/fr.yml
deleted file mode 100644
index 0936cdb..0000000
--- a/i18n/cli/fr.yml
+++ /dev/null
@@ -1,125 +0,0 @@
----
-fr:
-  error:
-    config:
-      write: "Impossible d'écrire le fichier de configuration!"
-      check: "Le fichier de configuration est invalide!"
-      key_bad_format: "La clé GPG est invalide!"
-    delete:
-      id_no_exist: "Impossible de supprimer l'élément %{id}, car il n'existe pas!"
-    export:
-      write: "Impossible d'exporter les données dans le fichier %{file}!"
-    gpg_file: 
-      decrypt: "Impossible de déchiffrer le fichier GPG!"
-      encrypt: "Impossible de chiffrer le fichier GPG!"
-    import:
-      bad_format: "Impossible d'importer le fichier car son format est incorrect!"
-      read: "Impossible d'importer le fichier %{file}, car il n'est pas lisible!"
-    update:
-      name_empty: "Vous devez définir un nom!"
-    sync: 
-      connection: "La connexion n'a pu être établie!"
-      communication: "Un problème de communication avec le serveur est apparu!"
-      no_data: "Aucune donnée!"
-      not_authorized: "Vous n'avez pas les autorisations d'accès au fichier distant!"
-      unknown: "Une erreur inconnue est survenue!" 
-  option:
-    usage: "Utilisation"
-    show: "Recherche et affiche les éléments"
-    show_all: "Affiche tous les éléments"
-    update: "Met à jour un élément"
-    remove: "Supprime un élément"
-    group: "Recherche les éléments appartenant au groupe spécifié"
-    add: "Ajoute un élément"
-    config: "Spécifie le fichier de configuration à utiliser"
-    setup: "Création d'un nouveau fichier de configuration" 
-    protocol: "Sélectionne les éléments ayant le protocole spécifié"
-    export: "Exporte tous les éléments dans un fichier au format CSV"
-    import: "Importe des éléments depuis un fichier au format CSV"
-    force: "Force une action, l'action ne demandera pas de confirmation"
-    format: "Change le format d'affichage des éléments par un alternatif"
-    generate_password: "Génére un mot de passe aléatoire (défaut 8 caractères)"
-    help: "Affiche ce message d'aide"
-  form:
-    add:
-      title: "Ajout d'un nouvel élément"
-      name: "Entrez le nom: "
-      group: "Entrez le groupe (optionnel): "
-      server: "Entrez the hostname or ip: "
-      protocol: "Entrez le protocole de connexion (ssh, http, other): "
-      login: "Entrez l'identifiant de connexion: "
-      password: "Entrez le mot de passe: "
-      port: "Entrez le port de connexion (optionnel): "
-      comment: "Entrez un commentaire (optionnel): "
-      valid: "L'élément a bien été ajouté!"
-    delete:
-      ask: "Êtes vous sûre de vouloir supprimer l'élément %{id} ?"
-      valid: "L'élément %{id} a bien été supprimé!"
-      not_valid: "L'élément %{id} n'a pu être supprimé, car il n'existe pas!"
-    import:
-      ask: "Êtes vous sûre de vouloir importer le fichier %{file} ?"
-      valid: "L'import est un succès!"
-      not_valid: "Aucune donnée à importer!"
-    setup:
-      title: "Création d'un nouveau fichier de configuration"
-      lang: "Choisissez votre langue (en, fr, ...) [défaut=%{lang}]: "
-      gpg_key: "Entrez la clé GPG: " 
-      gpg_file: "Entrez le chemin du fichier qui sera chiffré [défaut=%{home}/.mpw.gpg]: "
-      timeout: "Entrez le temps (en seconde) du mot de passe GPG [défaut=60]: "
-      sync_host: "Serveur de synchronisation: "
-      sync_port: "Port du serveur de synchronisation: "
-      sync_pwd: "Mot de passe pour la synchronisation: "
-      sync_suffix: "Suffix pour la synchronisation (optionnel): "
-      valid: "Le fichier de configuration a bien été créé!"
-    update:
-      title: "Mis à jour d'un élément"
-      name: "Entrez le nom [%{name}]: "
-      group: "Entrez le groupe [%{group}]: "
-      server: "Entrez le nom de domaine ou l'ip du serveur [%{server}]: "
-      protocol: "Entrez le protocole de connexion [%{protocol}]: "
-      login: "Entrez votre identifiant de connexion [%{login}]: "
-      password: "Entrez le mot de passe: "
-      port: "Entrez un port de connexion [%{port}]: "
-      comment: "Entrez un commentaire [%{comment}]: "
-      valid: "L'élément a bien été mis à jour!" 
-  interactive:
-    ask_password: "Mot de passe GPG: "
-    bad_password: "Mauvais mot de passe!"
-    goodbye: "Au revoir!"
-    unknown_command: "Commande inconnue!"
-    option:
-      title: "Aide"
-      show: "Cherche et affiche les résulats"
-      group: "Change de groupe pour la recherche"
-      add: "Ajout un élément"
-      update: "Met à jour un élément"
-      remove: "Supprime un élément"
-      help: "Affiche ce message d'aide"
-      quit: "Quitte le programme"
-  display:
-    comment: "Commentaire"
-    error: "ERREUR"
-    gpg_password: "Mot de passe GPG: "
-    group: "Groupe"
-    login: "Identifiant"
-    name: "Nom"
-    nothing: "Aucun résultat!"
-    password: "Mot de passe"
-    port: "Port"
-    protocol: "Protocol"
-    server: "Serveur"
-  ssh:
-    option:
-      usage: "Utilisation"
-      login: "Change l'identifiant de connexion"
-      server: "Change le nom de domaine ou l'ip du serveur"
-      port: "Change le port de connexion"
-      help: "Affiche ce message d'aide"
-    display:
-      connect: "Connexion à:"
-      nothing: "Aucun résultat!"
-  formats:
-    default: ! '%Y-%m-%d'
-    long: ! '%B %d, %Y'
-    short: ! '%b %d'
-    custom: ! '%A, %M %B, %Y @ %l:%M%P'
diff --git a/i18n/en.yml b/i18n/en.yml
new file mode 100644
index 0000000..9df7bda
--- /dev/null
+++ b/i18n/en.yml
@@ -0,0 +1,195 @@
+---
+en:
+  error:
+    bad_class: "The object class isn't valid!"
+    config:
+      write: "Can't write the config file!"
+      load: "Checkconfig failed!"
+      key_bad_format: "The key string isn't in the right format!"
+      no_key_public: "You haven't entered the public key of %{key}!"
+      genkey_gpg:
+        exception: "Can't create the GPG key!"
+        name: "You must define a name for your GPG key!"
+        password: "You must define a password for your GPG key!"
+    empty: "The class is empty"
+    export: "Can't export, unable to write in %{file}!"
+    export_key: "Can't export the GPG key"
+    gpg_file: 
+      decrypt: "Can't decrypt file!"
+      encrypt: "Can't encrypt the GPG file!"
+    mpw_file: 
+      read_data: "Can't read the MPW file!"
+      write_data: "Can't write the MPW file!"
+    import: "Can't import, unable to read %{file}!"
+    update:
+      host_and_comment_empty: "You must define a host or a comment!"
+
+  warning:
+    select: 'Your choice is not a valid item!'
+
+  command:
+    add: "Add a new item"
+    config: "Manage the general config"
+    copy: "Copy a login, password or OTP code"
+    delete: "Delete an item"
+    export: "Export the data in plain text"
+    genpwd: "Generate a password"
+    import: "Import data from a file"
+    list: "Print the items"
+    update: "Update an item"
+    wallet: "Manage the wallet config"
+
+  option:
+    add: "Add an item or key"
+    add_gpg_key: "Share the wallet with another GPG key"
+    alpha: "Use letter to create a password"
+    comment: "Specify a comment"
+    config: "Specify the configuration file to use"
+    clipboard: "Disable the clipboard feature"
+    default_path: "Move the wallet to the default directory"
+    default_wallet: "Specify the default wallet to use"
+    delete_gpg_key: "Delete wallet sharing with an other GPG key"
+    disable_alpha: "Don't use letters to create a password"
+    disable_numeric: "Don't use numbers to generate a password"
+    disable_pinmode: "Disable the pinentry mode"
+    disable_special_chars: "Don't use special char to create a password"
+    export: "Export a wallet in an yaml file"
+    file_export: "Specify the file to export data"
+    file_format: "Format of import file (default: mpw; available: %{formats})"
+    file_import: "Specify the file to import"
+    force: "Do not ask confirmation when deleting an item"
+    generate_password: "Create a random password (default 8 characters)"
+    gpg_exe: "Set the gpg binary path to use"
+    gpg_key: "Specify a GPG key (ex: user@example.com)"
+    group: "Search the items with specified group"
+    help: "Show this help message"
+    init: "Initialize mpw"
+    import: "Import item from an yaml file"
+    key: "Define the key name"
+    lang: "Set the software language"
+    length: "Size of the password"
+    list: "List the wallets"
+    list_keys: "List the GPG keys in wallet"
+    new_group: "Define a group for the item"
+    numeric: "Use number to create a password"
+    otp_code: "Set an otp key"
+    path: "Move the wallet in new specify directory"
+    pattern: "Given search pattern"
+    pinmode: "Enable pinentry mode (available with gpg >= 2.1)"
+    random_password: "Generate a random password"
+    setup: "Create a new configuration file" 
+    setup_wallet: "Create a new configuration file for a wallet"
+    special_chars: "Use special char to create a password"
+    show: "Search and display the items"
+    show_all: "Listing all items"
+    text_editor: "Use text editor to edit the item"
+    usage: "Use"
+    url: "Set an url (ex: https://example.com/path)"
+    user: "Set an user"
+    wallet: "Specify a wallet to use"
+    wallet_dir: "Set the wallets folder"
+
+  form:
+    select:
+      choice: "Select the item: "
+      error: "No item selected"
+    add_key:
+      valid: "Key has been added!"
+    add_item:
+      name: "Item name (mandatory)"
+      group: "Group name"
+      host: "Hostname or ip"
+      protocol: "Connection protocol (ssh, http, ...)"
+      login: "Connection ID"
+      password: "Password"
+      port: "Connection port"
+      comment: "A comment"
+      otp_key: "The OTP secret"
+      valid: "Item has been added!"
+    clipboard:
+      choice: "What do you want to copy ? [q = quit, p = password, l = login]: "
+      clean: "The clipboard has been cleaned."
+      login: "The login has been copied in clipboard."
+      password: "The password has been copied in clipboard for 30s!"
+      otp: "The OTP code has been copied %{time}s!"
+      url: "The URL has been copied in clipboard."
+      help:
+         name: "Help"
+         url: "Press <u> to copy URL"
+         login: "Press <l> to copy the login"
+         password: "Press <p> to copy the password"
+         otp_code: "Press <o> to copy the otp code"
+         quit: "Press <q> to quit"
+    delete_key:
+      valid: "Key has been deleted!"
+    delete_item:
+      ask: "Are you sure you want to remove this item ?"
+      valid: "The item has been removed!"
+    import:
+      ask: "Are you sure you want to import this file %{file} ?"
+      file_empty: "The import file is empty!"
+      file_not_exist: "The import file doesn't exist!"
+      format_unknown: "The import format '%{file_format} is unknown!"
+      valid: "The import is successful!"
+      not_valid: "No data to import!"
+    set_config:
+      valid: "The config file has been edited!"
+    set_wallet_path:
+      valid: "The wallet has been moved!"
+    setup_config:
+      title: "Setup a new config file"
+      lang: "Choose your language (en, fr, ...) [default=%{lang}]: "
+      gpg_key: "Enter the GPG key [ex: test@host.local]: " 
+      gpg_exe: "Enter the executable GPG path (optional): "
+      wallet_dir: "Enter the wallets's folder path [default=%{home}/wallets]: "
+      valid: "The config file has been created!"
+    setup_gpg_key:
+      title: "Setup a GPG key"
+      ask: "Do you want to create your GPG key ? (Y/n)"
+      no_create: "You must to create manually your GPG key or relaunch the software."
+      name: "Your name and lastname: "
+      password: "A password for the GPG key: "
+      confirm_password: "Confirm your password: "
+      error_password: "Your passwords aren't identical!" 
+      length: "Size of the GPG key [default=2048]: "
+      expire: "Expire time of the GPG key [default=0 (unlimited)]: "
+      wait: "Please wait until GPG key is created, this process can take a few minutes."
+      valid: "Your GPG key has been created ;-)"
+    update_item:
+      name: "Item name (mandatory)"
+      group: "Group name"
+      host: "Hostname or ip"
+      protocol: "Connection protocol (ssh, http, ...)"
+      login: "Login id"
+      password: "Password (leave empty if you don't want to update it)"
+      port: "Connection port"
+      comment: "A comment"
+      otp_key: "Secret OTP (leave empty if you don't want to update it"
+      valid: "Item has been updated!" 
+    export:
+      valid: "The export in %{file} is successful!"
+
+  display:
+    comment: "Comment"
+    config: "Configuration"
+    error: "ERROR"
+    keys: "GPG keys"
+    gpg_password: "GPG password: "
+    group: "Group"
+    login: "Login"
+    name: "Name"
+    no_group: "Without group"
+    nothing: "No matches!"
+    otp_code: "OTP code"
+    password: "Password"
+    port: "Port"
+    protocol: "Protocol"
+    server: "Server"
+    wallets: "Wallets"
+    warning: "Warning"
+
+  formats:
+    default: ! '%Y-%m-%d'
+    long: ! '%B %d, %Y'
+    short: ! '%b %d'
+    custom: ! '%A, %M %B, %Y @ %l:%M%P'
diff --git a/i18n/fr.yml b/i18n/fr.yml
new file mode 100644
index 0000000..04583ad
--- /dev/null
+++ b/i18n/fr.yml
@@ -0,0 +1,195 @@
+---
+fr:
+  error:
+    bad_class: "La classe de l'objet n'est pas celle attendue!"
+    config:
+      write: "Impossible d'écrire le fichier de configuration!"
+      load: "Le fichier de configuration est invalide!"
+      key_bad_format: "La clé GPG est invalide!"
+      no_key_public: "Vous ne possédez pas la clé publique de %{key}!"
+      genkey_gpg:
+        exception: "La création de la clé GPG n'a pas pu aboutir!"
+        name: "Vous devez définir un nom pour votre clé GPG!"
+        password: "Vous devez définir un mot de passe pour votre clé GPG!"
+    empty: "La classe est vide"
+    export: "Impossible d'exporter les données dans le fichier %{file}!"
+    export_key: "Impossible d'exporter la clé GPG"
+    gpg_file: 
+      decrypt: "Impossible de déchiffrer le fichier GPG!"
+      encrypt: "Impossible de chiffrer le fichier GPG!"
+    mpw_file: 
+      read_data: "Impossible de lire le fichier MPW!"
+      write_data: "Impossible d'écrire le fichier MPW!"
+    import: "Impossible d'importer le fichier %{file}, car il n'est pas lisible!"
+    update:
+      host_and_comment_empty: "Vous devez définir un host ou un commentaire!"
+
+  warning:
+    select: "Votre choix n'est pas un élément valide!"
+
+  command:
+    add: "Ajoute un nouvel élément"
+    config: "Gère la configuration générale"
+    copy: "Copie un identifiant,  un mot de passe ou un code OTP"
+    delete: "Supprimer un élément d'un portefeuille"
+    export: "Exporte les données"
+    genpwd: "Génére un mot de passe"
+    import: "Importe des données"
+    list: "Liste les éléments d'un portefeuille"
+    update: "Met à jour un élément"
+    wallet: "Gére la configuration d'un portefeuille"
+
+  option:
+    add: "Ajoute un élément ou une clé"
+    add_gpg_key: "Partage le portefeuille avec une autre clé GPG"
+    alpha: "Utilise des lettres dans la génération d'un mot de passe"
+    config: "Spécifie le fichier de configuration à utiliser"
+    comment: "Spécifie un commentaire"
+    clipboard: "Désactive la fonction presse papier"
+    default_path: "Déplace le portefeuille dans le dossier par défaut"
+    default_wallet: "Spécifie le porte-feuille à utiliser par défaut"
+    delete_gpg_key: "Supprime le partage le portefeuille avec une autre clé GPG"
+    disable_alpha: "Désactive l'utilisation des lettres dans la génération d'un mot de passe"
+    disable_numeric: "Désactive l'utilisation des chiffre dans la génération d'un mot de passe"
+    disable_pinmode: "Désactive le mode pinentry"
+    disable_special_chars: "Désactive l'utilisation des charactères speciaux dans la génération d'un mot de passe"
+    export: "Exporte un portefeuille dans un fichier yaml"
+    file_export: "Spécifie le fichier où exporter les données"
+    file_format: "Format du fichier d'import (défault: mpw; disponible: %{formats})"
+    file_import: "Spécifie le fichier à importer"
+    force: "Ne demande pas de confirmation pour la suppression d'un élément"
+    generate_password: "Génére un mot de passe aléatoire (défaut 8 caractères)"
+    gpg_exe: "Spécifie le chemin du binaire gpg à utiliser"
+    gpg_key: "Spécifie une clé GPG (ex: user@example.com)"
+    group: "Recherche les éléments appartenant au groupe spécifié"
+    help: "Affiche ce message d'aide"
+    import: "Importe des éléments depuis un fichier yaml"
+    init: "Initialise mpw"
+    key: "Spécifie le nom d'une clé"
+    lang: "Spécifie la langue du logiciel (ex: fr)"
+    length: "Taille du mot de passe"
+    list: "Liste les portefeuilles"
+    list_keys: "Liste les clés GPG dans le portefeuille"
+    new_group: "Spécifie le groupe de l'item"
+    numeric: "Utilise des chiffre dans la génération d'un mot de passe"
+    otp_code: "Spécifie un code OTP"
+    path: "Déplace le portefeuille dans un nouveau dossier"
+    pattern: "Motif de donnée à chercher"
+    pinmode: "Active le mode pinentry (valable avec gpg >= 2.1)"
+    random_password: "Génére un mot de passe aléatoire"
+    setup: "Création d'un nouveau fichier de configuration"
+    setup_wallet: "Création d'un nouveau fichier de configuration pour un portefeuille"
+    special_chars: "Utilise des charactères speciaux dans la génération d'un mot de passe"
+    show: "Recherche et affiche les éléments"
+    show_all: "Liste tous les éléments"
+    text_editor: "Active l'édition avec un éditeur de texte"
+    usage: "Utilisation"
+    url: "Spécifie l'url (ex: http://example.com/path)"
+    user: "Spécifie un utilisateur"
+    wallet: "Spécifie le portefeuille à utiliser"
+    wallet_dir: "Spécifie le répertoire des portefeuilles"
+
+  form:
+    select:
+      choice: "Sélectionner l'élément: "
+      error: "Aucun élément sélectionné"
+    add_key:
+      valid: "La clé a bien été ajoutée!"
+    add_item:
+      name: "Le nom de l'élément (obligatoire)"
+      group: "Le nom du groupe"
+      host: "Le nom de domaine ou l'ip"
+      protocol: "Le protocole de connexion (ssh, http, ...)"
+      login: "L'identifiant de connexion"
+      password: "Le mot de passe"
+      port: "Le port de connexion"
+      comment: "Un commentaire"
+      otp_key: "Le secret OTP"
+      valid: "L'élément a bien été ajouté!"
+    clipboard:
+      choice: "Que voulez-vous copier ? : "
+      clean: "Le presse papier a été nettoyé."
+      login: "L'identifiant a été copié dans le presse papier"
+      password: "Le mot de passe a été copié dans le presse papier pour 30s!"
+      otp: "Le code OTP a été copié dans le presse papier il est valable %{time}s!"
+      url: "L'URL a été copié dans le presse papier"
+      help:
+         name: "Aide"
+         url: "Pressez <u> pour copier l'URL"
+         login: "Pressez <l> pour copier l'identifiant"
+         password: "Pressez <p> pour copier le mot de passe"
+         otp_code: "Pressez <o> pour copier le code OTP"
+         quit: "Pressez <q> pour quitter"
+    delete_key:
+      valid: "La clé a bien été supprimée!"
+    delete_item:
+      ask: "Êtes vous sûre de vouloir supprimer l'élément ?"
+      valid: "L'élément a bien été supprimé!"
+    import:
+      ask: "Êtes vous sûre de vouloir importer le fichier %{file} ?"
+      file_empty: "Le fichier d'import est vide!"
+      file_not_exist: "Le fichier d'import n'existe pas"
+      format_unknown: "Le format d'import '%{file_format}' est inconnu!"
+      valid: "L'import est un succès!"
+      not_valid: "Aucune donnée à importer!"
+    set_config:
+      valid: "Le fichier de configuration a bien été modifié!"
+    set_wallet_path:
+      valid: "Le portefeuille a bien été déplacé!"
+    setup_config:
+      title: "Création d'un nouveau fichier de configuration"
+      lang: "Choisissez votre langue (en, fr, ...) [défaut=%{lang}]: "
+      gpg_key: "Entrez la clé GPG [ex: test@host.local]: " 
+      gpg_exe: "Entrez le chemin de l'exécutable GPG (optionnel): "
+      wallet_dir: "Entrez le chemin du répertoire qui contiendra les porte-feuilles de mot de passe [défaut=%{home}/wallets]: "
+      valid: "Le fichier de configuration a bien été créé!"
+    setup_gpg_key:
+      title: "Configuration d'une nouvelle clé GPG"
+      ask: "Voulez vous créer votre clé GPG ? (O/n)"
+      no_create: "Veuillez créer manuellement votre clé GPG ou relancer le logiciel."
+      name: "Votre nom et prénom: "
+      password: "Mot de passe de la clé GPG: "
+      confirm_password: "Retapez votre mot de passe: "
+      error_password: "Vos deux mots de passes ne sont pas identiques!"
+      length: "Taille de la clé GPG [défaut=2048]: "
+      expire: "Expiration de la clé GPG [défaut=0 (illimité)]: "
+      wait: "Veuillez patienter durant la génération de votre clé GPG, ce processus peut prendre quelques minutes."
+      valid: "Votre clé GPG a bien été créée ;-)"
+    update_item:
+      name: "Le nom de l'élément (obligatoire)"
+      group: "Le nom du groupe"
+      host: "Le nom de domaine ou l'ip"
+      protocol: "Le protocole de connexion (ssh, http, ...)"
+      login: "L'identifiant de connexion"
+      password: "Le mot de passe (laissez vide si vous ne voulez pas le changer)"
+      port: "Le port de connexion"
+      comment: "Un commentaire"
+      otp_key: "Le secret OTP (laissez vide si vous ne voulez pas le changer)"
+      valid: "L'élément a bien été mis à jour!" 
+    export:
+      valid: "L'export dans %{file} est un succès!"
+
+  display:
+    comment: "Commentaire"
+    config: "Configuration"
+    error: "ERREUR"
+    keys: "Clés GPG"
+    gpg_password: "Mot de passe GPG: "
+    group: "Groupe"
+    login: "Identifiant"
+    name: "Nom"
+    no_group: "Sans groupe"
+    nothing: "Aucun résultat!"
+    otp_code: "Code OTP"
+    password: "Mot de passe"
+    port: "Port"
+    protocol: "Protocol"
+    server: "Serveur"
+    wallets: "Porte-feuilles"
+    warning: "Warning"
+
+  formats:
+    default: ! '%Y-%m-%d'
+    long: ! '%B %d, %Y'
+    short: ! '%b %d'
+    custom: ! '%A, %M %B, %Y @ %l:%M%P'
diff --git a/i18n/server/en.yml b/i18n/server/en.yml
deleted file mode 100644
index df88dfe..0000000
--- a/i18n/server/en.yml
+++ /dev/null
@@ -1,26 +0,0 @@
----
-en:
-  option:
-    usage: "Usage"
-    config: "Specifie the configuration file"
-    checkconfig: "Check the configuration"
-    setup: "Setup a new configuration file"
-    help: "Show this message help"
-  checkconfig:
-    fail: "Checkconfig failed:!"
-    empty: "ERROR: an importe option is missing!"
-    datadir: "ERROR: le data directory doesn't exist!"
-  form:
-    setup:
-      title: "Serveur configuration" 
-      host: "IP listen: "
-      port: "Port listen: "
-      data_dir: "Data directory: "
-      timeout: "Timeout to second: "
-      log_file: "Log file path: "
-      not_valid: "ERROR: Impossible to write the configuration file!"
-  formats:
-    default: ! '%Y-%m-%d'
-    long: ! '%B %d, %Y'
-    short: ! '%b %d'
-    custom: ! '%A, %M %B, %Y @ %l:%M%P'
diff --git a/i18n/server/fr.yml b/i18n/server/fr.yml
deleted file mode 100644
index 667757c..0000000
--- a/i18n/server/fr.yml
+++ /dev/null
@@ -1,26 +0,0 @@
----
-fr:
-  option:
-    usage: "Utilisation"
-    config: "Spécifie le fichier de configuration"
-    checkconfig: "Vérifie le fichier de configuration"
-    setup: "Permet de générer un nouveau fichier de configuration"
-    help: "Affiche ce message d'aide"
-  checkconfig:
-    fail: "Le fichier de configuration est invalide!"
-    empty: "ERREUR: Une option importante est manquante!"
-    datadir: "ERREUR: Le répertoire des données n'existe pas!"
-  form:
-    setup:
-      title: "Configuration du serveur" 
-      host: "IP d'écoute: "
-      port: "Port d'écoute: "
-      data_dir: "Répertoire des données: "
-      log_file: "Chemin du ficier de log: "
-      timeout: "Timeout en seconde: "
-      not_valid: "ERREUR: Impossible d'écire le fichier de configuration!"
-  formats:
-    default: ! '%Y-%m-%d'
-    long: ! '%B %d, %Y'
-    short: ! '%b %d'
-    custom: ! '%A, %M %B, %Y @ %l:%M%P'
diff --git a/lib/Cli.rb b/lib/Cli.rb
deleted file mode 100644
index 74f966b..0000000
--- a/lib/Cli.rb
+++ /dev/null
@@ -1,360 +0,0 @@
-#!/usr/bin/ruby
-# author: nishiki
-# mail: nishiki@yaegashi.fr
-# info: a simple script who m your passwords
-
-require 'rubygems'
-require 'highline/import'
-require 'pathname'
-require 'readline'
-require 'i18n'
-require 'yaml'
-
-require "#{APP_ROOT}/lib/MPW.rb"
-require "#{APP_ROOT}/lib/MPWConfig.rb"
-require "#{APP_ROOT}/lib/Sync.rb"
-
-class Cli
-
-	# Constructor
-	# @args: lang -> the operating system language
-	#        config_file -> a specify config file
-	def initialize(lang, config)
-		@config = config
-	end
-
-	# Close sync
-	def sync_close()
-		@sync.close()
-	end
-
-	# Sync the data with the server
-	# @rtnr: true if the synchro is finish
-	def sync()
-		if !defined?(@sync)
-			@sync = Sync.new()
-
-			if !@config.sync_host.nil? && !@config.sync_port.nil?
-				if !@sync.connect(@config.sync_host, @config.sync_port, @config.key, @config.sync_pwd, @config.sync_suffix)
-					puts "#{I18n.t('display.error')}: #{@sync.error_msg}"
-				end
-			end
-		end
-		
-		begin
-			if @sync.enable
-				if !@mpw.sync(@sync.get(@passwd), @config.last_update)
-					puts "#{I18n.t('display.error')}: #{@mpw.error_msg}"
-				elsif !@sync.update(File.open(@config.file_gpg).read)
-					puts "#{I18n.t('display.error')}: #{@sync.error_msg}"
-				elsif !@config.set_last_update()
-					puts "#{I18n.t('display.error')}: #{@config.error_msg}"
-				else
-					return true
-				end
-			end
-		rescue Exception => e
-			puts "#{I18n.t('display.error')}: #{e}"
-		end
-
-		return false
-	end
-
-	# Create a new config file
-	# @args: lang -> the software language
-	def setup(lang)
-		puts I18n.t('form.setup.title')
-		puts '--------------------'
-		language    = ask(I18n.t('form.setup.lang', :lang => lang)).to_s
-		key         = ask(I18n.t('form.setup.gpg_key')).to_s
-		file_gpg    = ask(I18n.t('form.setup.gpg_file', :home => Dir.home())).to_s
-		timeout_pwd = ask(I18n.t('form.setup.timeout')).to_s
-		sync_host   = ask(I18n.t('form.setup.sync_host')).to_s
-		sync_port   = ask(I18n.t('form.setup.sync_port')).to_s
-		sync_pwd    = ask(I18n.t('form.setup.sync_pwd')).to_s
-		sync_suffix = ask(I18n.t('form.setup.sync_suffix')).to_s
-		
-		if !File.exist?("#{APP_ROOT}/i18n/#{language}.yml")
-			language= 'en'
-		end
-		I18n.locale = language.to_sym
-
-		sync_host.empty?   ? (sync_host = nil)   : (sync_host   = sync_host)
-		sync_port.empty?   ? (sync_port = nil)   : (sync_port   = sync_port.to_i)
-		sync_pwd.empty?    ? (sync_pwd = nil)    : (sync_pwd    = sync_pwd)
-		sync_suffix.empty? ? (sync_suffix = nil) : (sync_suffix = sync_suffix)
-
-		if @config.setup(key, language, file_gpg, timeout_pwd, sync_host, sync_port, sync_pwd, sync_suffix)
-			puts I18n.t('form.setup.valid')
-		else
-			puts "#{I18n.t('display.error')}: #{@config.error_msg}"
-		end
-
-		if not @config.checkconfig()
-			puts "#{I18n.t('display.error')}: #{@config.error_msg}"
-			exit 2
-		end
-	end
-
-	# Request the GPG password and decrypt the file
-	def decrypt()
-		if !defined?(@mpw)
-			@mpw = MPW.new(@config.file_gpg, @config.key)
-		end
-
-		@passwd = ask(I18n.t('display.gpg_password')) {|q| q.echo = false}
-		if !@mpw.decrypt(@passwd)
-			puts "#{I18n.t('display.error')}: #{@mpw.error_msg}"
-			exit 2
-		end
-	end
-
-	# Display the query's result
-	# @args: search -> the string to search
-	#        protocol -> search from a particular protocol
-	def display(search, protocol=nil, group=nil, format=nil)
-		result = @mpw.search(search, group, protocol)
-
-		if not result.empty?
-			result.each do |r|
-				if format.nil? || !format
-					displayFormat(r)
-				else
-					displayFormatAlt(r)
-				end
-			end
-		else
-			puts I18n.t('display.nothing')
-		end
-	end
-
-	# Display an item in the default format
-	# @args: item -> an array with the item information
-	def displayFormat(item)
-		puts '--------------------'
-		puts "Id: #{item[MPW::ID]}"
-		puts "#{I18n.t('display.name')}: #{item[MPW::NAME]}"
-		puts "#{I18n.t('display.group')}: #{item[MPW::GROUP]}"
-		puts "#{I18n.t('display.server')}: #{item[MPW::SERVER]}"
-		puts "#{I18n.t('display.protocol')}: #{item[MPW::PROTOCOL]}"
-		puts "#{I18n.t('display.login')}: #{item[MPW::LOGIN]}"
-		puts "#{I18n.t('display.password')}: #{item[MPW::PASSWORD]}"
-		puts "#{I18n.t('display.port')}: #{item[MPW::PORT]}"
-		puts "#{I18n.t('display.comment')}: #{item[MPW::COMMENT]}"
-	end
-
-	# Display an item in the alternative format
-	# @args: item -> an array with the item information
-	def displayFormatAlt(item)
-		item[MPW::PORT].nil? ? (port = '') : (port = ":#{item[MPW::PORT]}")
-
-		if item[MPW::PASSWORD].nil? || item[MPW::PASSWORD].empty?
-			if item[MPW::LOGIN].include('@')
-				puts "# #{item[MPW::ID]} #{item[MPW::PROTOCOL]}://#{item[MPW::LOGIN]}@#{item[MPW::SERVER]}#{port}"
-			else
-				puts "# #{item[MPW::ID]} #{item[MPW::PROTOCOL]}://{#{item[MPW::LOGIN]}}@#{item[MPW::SERVER]}#{port}"
-			end
-		else
-			puts "# #{item[MPW::ID]} #{item[MPW::PROTOCOL]}://{#{item[MPW::LOGIN]}:#{item[MPW::PASSWORD]}}@#{item[MPW::SERVER]}#{port}"
-		end
-	end
-
-	# Form to add a new item
-	def add()
-		row = Array.new()
-		puts I18n.t('form.add.title')
-		puts '--------------------'
-		name     = ask(I18n.t('form.add.name')).to_s
-		group    = ask(I18n.t('form.add.group')).to_s
-		server   = ask(I18n.t('form.add.server')).to_s
-		protocol = ask(I18n.t('form.add.protocol')).to_s
-		login    = ask(I18n.t('form.add.login')).to_s
-		passwd   = ask(I18n.t('form.add.password')).to_s
-		port     = ask(I18n.t('form.add.port')).to_s
-		comment  = ask(I18n.t('form.add.comment')).to_s
-
-		if @mpw.update(name, group, server, protocol, login, passwd, port, comment)
-			if @mpw.encrypt()
-				sync()
-				puts I18n.t('form.add.valid')
-			else
-				puts "#{I18n.t('display.error')}: #{@mpw.error_msg}"
-			end
-		else
-			puts "#{I18n.t('display.error')}: #{@mpw.error_msg}"
-		end
-	end
-
-	# Update an item
-	# @args: id -> the item's id
-	def update(id)
-		row = @mpw.search_by_id(id)
-
-		if not row.empty?
-			puts I18n.t('form.update.title')
-			puts '--------------------'
-			name     = ask(I18n.t('form.update.name'    , :name => row[MPW::NAME])).to_s
-			group    = ask(I18n.t('form.update.group'   , :group => row[MPW::GROUP])).to_s
-			server   = ask(I18n.t('form.update.server'  , :server => row[MPW::SERVER])).to_s
-			protocol = ask(I18n.t('form.update.protocol', :protocol => row[MPW::PROTOCOL])).to_s
-			login    = ask(I18n.t('form.update.login'   , :login => row[MPW::LOGIN])).to_s
-			passwd   = ask(I18n.t('form.update.password')).to_s
-			port     = ask(I18n.t('form.update.port'    , :port => row[MPW::PORT])).to_s
-			comment  = ask(I18n.t('form.update.comment' , :comment => row[MPW::COMMENT])).to_s
-				
-			if @mpw.update(name, group, server, protocol, login, passwd, port, comment, id)
-				if @mpw.encrypt()
-					sync()
-					puts I18n.t('form.update.valid')
-				else
-					puts "#{I18n.t('display.error')}: #{@mpw.error_msg}"
-				end
-			else
-				puts "#{I18n.t('display.error')}: #{@mpw.error_msg}"
-			end
-		else
-			puts I18n.t('display.nothing')
-		end
-	end
-
-	# Remove an item
-	# @args: id -> the item's id
-	#        force -> no resquest a validation
-	def remove(id, force=false)
-		if not force
-			result = @mpw.search_by_id(id)
-
-			if result.length > 0
-				displayFormat(result)
-
-				confirm = ask("#{I18n.t('form.delete.ask', :id => id)} (y/N) ").to_s
-				if confirm =~ /^(y|yes|YES|Yes|Y)$/
-					force = true
-				end
-			else
-				puts I18n.t('display.nothing')
-			end
-		end
-
-		if force
-			if @mpw.remove(id)
-				if @mpw.encrypt()
-					sync()
-					puts I18n.t('form.delete.valid', :id => id)
-				else
-					puts "#{I18n.t('display.error')}: #{@mpw.error_msg}"
-				end
-			else
-				puts I18n.t('form.delete.not_valid')
-			end
-		end
-	end
-
-	# Export the items in a CSV file
-	# @args: file -> the destination file
-	def export(file)
-		if @mpw.export(file)
-			puts "The export in #{file} is succesfull!"
-		else
-			puts "#{I18n.t('display.error')}: #{@mpw.error_msg}"
-		end
-
-	end
-
-	# Import items from a CSV file
-	# @args: file -> the import file
-	#        force -> no resquest a validation
-	def import(file, force=false)
-		result = @mpw.import_preview(file)
-
-		if not force
-			if result.is_a?(Array) && !result.empty?
-				result.each do |r|
-					displayFormat(r)
-				end
-
-				confirm = ask("#{I18n.t('form.import.ask', :file => file)} (y/N) ").to_s
-				if confirm =~ /^(y|yes|YES|Yes|Y)$/
-					force = true
-				end
-			else
-				puts I18n.t('form.import.not_valid')
-			end
-		end
-
-		if force
-			if @mpw.import(file) && @mpw.encrypt()
-				sync()
-				puts I18n.t('form.import.valid')
-			else
-				puts "#{I18n.t('display.error')}: #{@mpw.error_msg}"
-			end
-		end
-	end
-
-	# Interactive mode
-	def interactive()
-		group       = nil
-		last_access = Time.now.to_i
-
-		while buf = Readline.readline('<mpw> ', true)
-
-			if @config.timeout_pwd < Time.now.to_i - last_access
-				passwd_confirm = ask(I18n.t('interactive.ask_password')) {|q| q.echo = false}
-
-				if @passwd.eql?(passwd_confirm)
-					last_access = Time.now.to_i
-				else
-					puts I18n.t('interactive.bad_password')
-					next
-				end
-			else
-				last_access = Time.now.to_i
-			end
-
-			command = buf.split(' ')
-
-			case command[0]
-			when 'display', 'show', 'd', 's'
-				if !command[1].nil? && !command[1].empty?
-					display(command[1], group, command[2])
-				end
-			when 'add', 'a'
-				add()
-			when 'update', 'u'
-				if !command[1].nil? && !command[1].empty?
-					update(command[1])
-				end
-			when 'remove', 'delete', 'r', 'd'
-				if !command[1].nil? && !command[1].empty?
-					remove(command[1])
-				end
-			when 'group', 'g'
-				if !command[1].nil? && !command[1].empty?
-					group = command[1]
-				else
-					group = nil
-				end
-			when 'help', 'h', '?'
-				puts I18n.t('interactive.option.title')
-				puts '--------------------'
-				puts "display, show, d, s SEARCH    #{I18n.t('interactive.option.show')}"
-				puts "group, g                      #{I18n.t('interactive.option.group')}"
-				puts "add, a                        #{I18n.t('interactive.option.add')}"
-				puts "update, u ID                  #{I18n.t('interactive.option.update')}"
-				puts "remove, delete, r, d ID       #{I18n.t('interactive.option.remove')}"
-				puts "help, h, ?                    #{I18n.t('interactive.option.help')}"
-				puts "quit, exit, q                 #{I18n.t('interactive.option.quit')}"
-			when 'quit', 'exit', 'q'
-				puts I18n.t('interactive.goodbye')
-				break
-			else
-				if !command[0].nil? && !command[0].empty?
-					puts I18n.t('interactive.unknown_command')
-				end
-			end
-
-		end
-
-	end
-end
diff --git a/lib/CliSSH.rb b/lib/CliSSH.rb
deleted file mode 100644
index 7fba1ad..0000000
--- a/lib/CliSSH.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-#!/usr/bin/ruby
-# author: nishiki
-# mail: nishiki@yaegashi.fr
-# info: a simple script who manage your passwords
-
-require "#{APP_ROOT}/lib/Cli.rb"
-
-class CliSSH < Cli
-
-	attr_accessor :server, :port, :login
-
-	# Connect to SSH
-	# args: search -> string to search
-	def ssh(search)
-		result = @mpw.search(search, nil, 'ssh')
-
-		if result.length > 0
-			result.each do |r|
-				@server.nil? ? (server = r[MPW::SERVER]) : (server = @server)
-				@port.nil?   ? (port   = r[MPW::PORT])   : (port   = @port)
-				@login.nil?  ? (login  = r[MPW::LOGIN])  : (login  = @login)
-
-				passwd = r[MPW::PASSWORD]
-
-				if port.nil? || port.empty?
-					port = 22
-				end
-
-				puts "#{I18n.t('ssh.display.connect')} ssh #{login}@#{server} -p #{port}"
-				if passwd.empty?
-					system("ssh #{login}@#{server} -p #{port}")
-				else
-					system("sshpass -p '#{passwd}' ssh #{login}@#{server} -p #{port}")
-				end
-			end
-
-		else
-			puts I18n.t('ssh.display.nothing')
-		end
-	end
-end
-
diff --git a/lib/MPW.rb b/lib/MPW.rb
deleted file mode 100644
index 366f657..0000000
--- a/lib/MPW.rb
+++ /dev/null
@@ -1,331 +0,0 @@
-#!/usr/bin/ruby
-# author: nishiki
-# mail: nishiki@yaegashi.fr
-# info: a simple script who manage your passwords
-
-require 'rubygems'
-require 'gpgme'
-require 'csv'
-require 'i18n'
-
-class MPW
-	
-	ID       = 0
-	NAME     = 1
-	GROUP    = 2
-	PROTOCOL = 3
-	SERVER   = 4
-	LOGIN    = 5
-	PASSWORD = 6
-	PORT     = 7
-	COMMENT  = 8
-	DATE     = 9
-
-	attr_accessor :error_msg
-	
-	# Constructor
-	def initialize(file_gpg, key=nil)
-		@error_msg = nil
-		@file_gpg  = file_gpg
-		@key       = key
-	end
-
-	# Decrypt a gpg file
-	# @args: password -> the GPG key password
-	# @rtrn: true if data has been decrypted
-	def decrypt(passwd=nil)
-		@data = Array.new
-
-		begin
-			if File.exist?(@file_gpg)
-				crypto = GPGME::Crypto.new(:armor => true)
-				data_decrypt = crypto.decrypt(IO.read(@file_gpg), :password => passwd).read
-
-				data_decrypt.lines do |line|
-					@data.push(line.parse_csv)
-				end
-			end
-
-			return true
-		rescue Exception => e 
-			@error_msg = "#{I18n.t('error.gpg_file.decrypt')}\n#{e}"
-			return false
-		end
-	end
-
-	# Encrypt a file
-	# @rtrn: true if the file has been encrypted
-	def encrypt()
-		begin
-			crypto = GPGME::Crypto.new(:armor => true)
-			file_gpg = File.open(@file_gpg, 'w+')
-
-			data_to_encrypt = ''
-			@data.each do |row|
-				data_to_encrypt << row.to_csv
-			end
-
-			crypto.encrypt(data_to_encrypt, :recipients => @key, :output => file_gpg)
-			file_gpg.close
-
-			return true
-		rescue Exception => e 
-			@error_msg = "#{I18n.t('error.gpg_file.encrypt')}\n#{e}"
-			return false
-		end
-	end
-	
-	# Search in some csv data
-	# @args: search -> the string to search
-	#        protocol -> the connection protocol (ssh, web, other)
-	# @rtrn: a list with the resultat of the search
-	def search(search='', group=nil, protocol=nil)
-		result = Array.new()
-
-		if !search.nil?
-			search = search.downcase
-		end
-		search = search.force_encoding('ASCII-8BIT')
-
-		@data.each do |row|
-			row[NAME].nil?    ? (name    = nil) : (name    = row[NAME].downcase)
-			row[SERVER].nil?  ? (server  = nil) : (server  = row[SERVER].downcase)
-			row[COMMENT].nil? ? (comment = nil) : (comment = row[COMMENT].downcase)
-
-			if name =~ /^.*#{search}.*$/  || server =~ /^.*#{search}.*$/ || comment =~ /^.*#{search}.*$/ 
-				if (protocol.nil? || protocol.eql?(row[PROTOCOL])) && (group.nil? || group.eql?(row[GROUP]))
-					result.push(row)
-				end
-			end
-		end
-
-		return result
-	end
-
-	# Search in some csv data
-	# @args: id -> the id item
-	# @rtrn: a row with the resultat of the search
-	def search_by_id(id)
-		@data.each do |row|
-			if row[ID] == id
-				return row
-			end
-		end
-
-		return Array.new()
-	end
-
-	# Update an item
-	# @args: id -> the item's identifiant
-	#        name -> the item name
-	#        group ->  the item group
-	#        server -> the ip or hostname
-	#        protocol -> the protocol
-	#        login -> the login
-	#        passwd -> the password
-	#        port -> the port
-	#        comment -> a comment
-	# @rtrn: true if the item has been updated
-	def update(name, group, server, protocol, login, passwd, port, comment, id=nil)
-		row    = Array.new()
-		update = false
-
-		i  = 0
-		@data.each do |r|
-			if r[ID] == id
-				row    = r
-				update = true
-				break
-			end
-			i += 1
-		end
-
-		if port.to_i <= 0
-			port = nil
-		end
-
-		row_update       = Array.new()
-		row_update[DATE] = Time.now.to_i
-
-		id.nil?	      || id.empty?       ? (row_update[ID]       = MPW.password(16)) : (row_update[ID]       = id)
-		name.nil?     || name.empty?     ? (row_update[NAME]     = row[NAME])        : (row_update[NAME]     = name)
-		group.nil?    || group.empty?    ? (row_update[GROUP]    = row[GROUP])       : (row_update[GROUP]    = group)
-		server.nil?   || server.empty?   ? (row_update[SERVER]   = row[SERVER])      : (row_update[SERVER]   = server)
-		protocol.nil? || protocol.empty? ? (row_update[PROTOCOL] = row[PROTOCOL])    : (row_update[PROTOCOL] = protocol)
-		login.nil?    || login.empty?    ? (row_update[LOGIN]    = row[LOGIN])       : (row_update[LOGIN]    = login)
-		passwd.nil?   || passwd.empty?   ? (row_update[PASSWORD] = row[PASSWORD])    : (row_update[PASSWORD] = passwd)
-		port.nil?     || port.empty?     ? (row_update[PORT]     = row[PORT])        : (row_update[PORT]     = port)
-		comment.nil?  || comment.empty?  ? (row_update[COMMENT]  = row[COMMENT])     : (row_update[COMMENT]  = comment)
-		
-		row_update[NAME].nil?     ? (row_update[NAME]     = row_update[NAME])     : (row_update[NAME]     = row_update[NAME].force_encoding('ASCII-8BIT'))
-		row_update[GROUP].nil?    ? (row_update[GROUP]    = row_update[GROUP])    : (row_update[GROUP]    = row_update[GROUP].force_encoding('ASCII-8BIT'))
-		row_update[SERVER].nil?   ? (row_update[SERVER]   = row_update[SERVER])   : (row_update[SERVER]   = row_update[SERVER].force_encoding('ASCII-8BIT'))
-		row_update[PROTOCOL].nil? ? (row_update[PROTOCOL] = row_update[PROTOCOL]) : (row_update[PROTOCOL] = row_update[PROTOCOL].force_encoding('ASCII-8BIT'))
-		row_update[LOGIN].nil?    ? (row_update[LOGIN]    = row_update[LOGIN])    : (row_update[LOGIN]    = row_update[LOGIN].force_encoding('ASCII-8BIT'))
-		row_update[PASSWORD].nil? ? (row_update[PASSWORD] = row_update[PASSWORD]) : (row_update[PASSWORD] = row_update[PASSWORD].force_encoding('ASCII-8BIT'))
-		row_update[COMMENT].nil?  ? (row_update[COMMENT]  = row_update[COMMENT])  : (row_update[COMMENT]  = row_update[COMMENT].force_encoding('ASCII-8BIT'))
-
-		if row_update[NAME].nil? || row_update[NAME].empty?
-			@error_msg = I18n.t('error.update.name_empty')
-			return false
-		end
-
-		if update
-			@data[i] = row_update
-		else
-			@data.push(row_update)
-		end
-
-		return true
-	end
-	
-	# Remove an item 
-	# @args: id -> the item's identifiant
-	# @rtrn: true if the item has been deleted
-	def remove(id)
-		i = 0
-		@data.each do |row|
-			if row[ID] == id
-				@data.delete_at(i)
-				return true
-			end
-			i += 1
-		end
-
-		@error_msg = I18n.t('error.delete.id_no_exist', :id => id)
-		return false
-	end
-
-	# Export to csv
-	# @args: file -> a string to match
-	# @rtrn: true if export work
-	def export(file)
-		begin
-			File.open(file, 'w+') do |file|
-				@data.each do |row|
-					row.delete_at(ID).delete_at(DATE)
-					file << row.to_csv
-				end
-			end
-
-			return true
-		rescue Exception => e 
-			@error_msg = "#{I18n.t('error.export.write', :file => file)}\n#{e}"
-			return false
-		end
-	end
-
-	# Import to csv
-	# @args: file -> path to file import
-	# @rtrn: true if the import work
-	def import(file)
-		begin
-			data_new = IO.read(file)
-			data_new.lines do |line|
-				if not line =~ /(.*,){6}/
-					@error_msg = I18n.t('error.import.bad_format')
-					return false
-				else
-					row = line.parse_csv.unshift(0)
-					if not update(row[NAME], row[GROUP], row[SERVER], row[PROTOCOL], row[LOGIN], row[PASSWORD], row[PORT], row[COMMENT])
-						return false
-					end
-				end
-			end
-
-			return true
-		rescue Exception => e 
-			@error_msg = "#{I18n.t('error.import.read', :file => file)}\n#{e}"
-			return false
-		end
-	end
-
-	# Return a preview import 
-	# @args: file -> path to file import
-	# @rtrn: an array with the items to import, if there is an error return false
-	def import_preview(file)
-		begin
-			result = Array.new()
-			id = 0
-
-			data = IO.read(file)
-			data.lines do |line|
-				if not line =~ /(.*,){6}/
-					@error_msg = I18n.t('error.import.bad_format')
-					return false
-				else
-					result.push(line.parse_csv.unshift(id))
-				end
-
-				id += 1
-			end
-
-			return result
-		rescue Exception => e 
-			@error_msg = "#{I18n.t('error.import.read', :file => file)}\n#{e}"
-			return false
-		end
-	end
-
-	# Sync remote data and local data
-	# @args: data_remote -> array with the data remote
-	#        last_update -> last update
-	# @rtrn: false if data_remote is nil
-	def sync(data_remote, last_update)
-		if !data_remote.instance_of?(Array)
-			return false
-		end
-
-		@data.each do |l|
-			j = 0
-			update = false
-
-			# Update item
-			data_remote.each do |r|
-				if l[ID] == r[ID]
-					if l[DATE].to_i < r[DATE].to_i
-						update(r[NAME], r[GROUP], r[SERVER], r[PROTOCOL], r[LOGIN], r[PASSWORD], r[PORT], r[COMMENT], l[ID])
-					end
-					update = true
-					data_remote.delete_at(j)
-					break
-				end
-				j += 1
-			end
-
-			# Delete an old item
-			if !update && l[DATE].to_i < last_update
-				remove(l[ID])
-			end
-		end
-
-		# Add item
-		data_remote.each do |r|
-			if r[DATE].to_i > last_update
-				update(r[NAME], r[GROUP], r[SERVER], r[PROTOCOL], r[LOGIN], r[PASSWORD], r[PORT], r[COMMENT], r[ID])
-			end
-		end
-
-		return encrypt()
-	end
-
-	# Generate a random password
-	# @args: length -> the length password
-	# @rtrn: a random string
-	def self.password(length=8)
-		if length.to_i <= 0
-			length = 8
-		else
-			length = length.to_i
-		end
-
-		result = ''
-		while length > 62 do
-			result << ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(62).join
-			length -= 62
-		end
-		result << ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(length).join
-
-		return result
-	end
-end
diff --git a/lib/MPWConfig.rb b/lib/MPWConfig.rb
deleted file mode 100644
index 43fdc66..0000000
--- a/lib/MPWConfig.rb
+++ /dev/null
@@ -1,136 +0,0 @@
-#!/usr/bin/ruby
-# author: nishiki
-# mail: nishiki@yaegashi.fr
-# info: a simple script who manage your passwords
-
-require 'rubygems'
-require 'yaml'
-require 'i18n'
-
-class MPWConfig
-	
-	attr_accessor :error_msg
-
-	attr_accessor :key
-	attr_accessor :lang
-	attr_accessor :file_gpg
-	attr_accessor :timeout_pwd
-	attr_accessor :last_update
-	attr_accessor :sync_host
-	attr_accessor :sync_port
-	attr_accessor :sync_pwd
-	attr_accessor :sync_suffix
-	attr_accessor :last_update
-
-	# Constructor
-	# @args: file_config -> the specify config file
-	def initialize(file_config=nil)
-		@error_msg   = nil
-		@file_config = "#{Dir.home()}/.mpw.cfg"
-
-		if !file_config.nil? && !file_config.empty?
-			@file_config = file_config
-		end
-	end
-
-	# Create a new config file
-	# @args: key -> the gpg key to encrypt
-	#        lang -> the software language
-	#        file_gpg -> the file who is encrypted
-	#        timeout_pwd -> time to save the password
-	#        sync_host -> the server host for synchronization
-	#        sync_port -> the server port for synchronization
-	#        sync_pwd -> the password for synchronization
-	#        sync_suffix -> the suffix file (optionnal) 
-	# @rtrn: true if le config file is create
-	def setup(key, lang, file_gpg, timeout_pwd, sync_host=nil, sync_port=nil, sync_pwd=nil, sync_suffix=nil)
-
-		if not key =~ /[a-zA-Z0-9.-_]+\@[a-zA-Z0-9]+\.[a-zA-Z]+/
-			@error_msg = I18n.t('error.config.key_bad_format')
-			return false
-		end
-		
-		if file_gpg.empty?
-			file_gpg = "#{Dir.home()}/.mpw.gpg"
-		end
-
-		timeout_pwd.empty? ? (timeout_pwd = 60) : (timeout_pwd = timeout_pwd.to_i)
-
-		config = {'config' => {'key'         => key,
-		                       'lang'        => lang,
-		                       'file_gpg'    => file_gpg,
-		                       'timeout_pwd' => timeout_pwd,
-		                       'sync_host'   => sync_host,
-		                       'sync_port'   => sync_port,
-		                       'sync_pwd'    => sync_pwd,
-		                       'sync_suffix' => sync_suffix,
-		                       'last_update' => 0 }}
-
-		begin
-			File.open(@file_config, 'w') do |file|
-				file << config.to_yaml
-			end
-		rescue Exception => e 
-			@error_msg = "#{I18n.t('error.config.write')}\n#{e}"
-			return false
-		end
-
-		return true
-	end
-
-	# Check the config file
-	# @rtrn: true if the config file is correct
-	def checkconfig()
-		begin
-			config = YAML::load_file(@file_config)
-			@key         = config['config']['key']
-			@lang        = config['config']['lang']
-			@file_gpg    = config['config']['file_gpg']
-			@timeout_pwd = config['config']['timeout_pwd'].to_i
-			@sync_host   = config['config']['sync_host']
-			@sync_port   = config['config']['sync_port']
-			@sync_pwd    = config['config']['sync_pwd']
-			@sync_suffix = config['config']['sync_suffix']
-			@last_update = config['config']['last_update'].to_i
-
-			if @key.empty? || @file_gpg.empty? 
-				@error_msg = I18n.t('error.config.check')
-				return false
-			end
-
-			I18n.locale = @lang.to_sym
-
-		rescue Exception => e 
-			@error_msg = "#{I18n.t('error.config.check')}\n#{e}"
-			return false
-		end
-
-		return true
-	end
-
-	# Set the last update when there is a sync
-	# @rtrn: true is the file has been updated
-	def set_last_update()
-		config = {'config' => {'key'         => @key,
-		                       'lang'        => @lang,
-		                       'file_gpg'    => @file_gpg,
-		                       'timeout_pwd' => @timeout_pwd,
-		                       'sync_host'   => @sync_host,
-		                       'sync_port'   => @sync_port,
-		                       'sync_pwd'    => @sync_pwd,
-		                       'sync_suffix' => @sync_suffix,
-		                       'last_update' => Time.now.to_i }}
-
-		begin
-			File.open(@file_config, 'w') do |file|
-				file << config.to_yaml
-			end
-		rescue Exception => e 
-			@error_msg = "#{I18n.t('error.config.write')}\n#{e}"
-			return false
-		end
-
-		return true
-	end
-	
-end
diff --git a/lib/Server.rb b/lib/Server.rb
deleted file mode 100644
index 3baa22f..0000000
--- a/lib/Server.rb
+++ /dev/null
@@ -1,336 +0,0 @@
-#!/usr/bin/ruby
-
-require 'socket'
-require 'json'
-require 'highline/import'
-require 'digest'
-require 'logger'
-
-class Server
-
-	attr_accessor :error_msg
-
-	# Constructor
-	def initialize()
-		YAML::ENGINE.yamler='syck'
-	end
-
-	# Start the server
-	def start()
-		begin
-			server = TCPServer.open(@host, @port)
-			@log.info("The server is started on #{@host}:#{@port}")
-		rescue Exception => e
-			@log.error("Impossible to start the server: #{e}")
-			exit 2
-		end
-
-		loop do
-			Thread.start(server.accept) do |client|
-				@log.info("#{client.peeraddr[3]} is connected")
-
-				while true do
-					msg = get_client_msg(client)
-
-					if !msg
-						next
-					end
-					
-					if !msg['action'].nil? && msg['action'] == 'close'
-						@log.info("#{client.peeraddr[3]} is disconnected")
-						close_connection(client)
-					end
-
-					if msg['gpg_key'].nil? || msg['gpg_key'].empty? || msg['password'].nil? || msg['password'].empty?
-						@log.warning("#{client.peeraddr[3]} is disconnected because no password or no gpg_key")
-						close_connection(client)
-						next
-					end
-
-					case msg['action']
-					when 'get'
-						@log.debug("#{client.peeraddr[3]} GET gpg_key=#{msg['gpg_key']} suffix=#{msg['suffix']}")
-						client.puts get_file(msg)
-					when 'update'
-						@log.debug("#{client.peeraddr[3]} UPDATE gpg_key=#{msg['gpg_key']} suffix=#{msg['suffix']}")
-						client.puts update_file(msg)
-					when 'delete'
-						@log.debug("#{client.peeraddr[3]} DELETE gpg_key=#{msg['gpg_key']} suffix=#{msg['suffix']}")
-						client.puts delete_file(msg)
-					else
-						@log.warning("#{client.peeraddr[3]} is disconnected because unkwnow command")
-						send_msg = {:action      => 'unknown',
-						            :gpg_key     => msg['gpg_key'],
-						            :error       => 'server.error.client.unknown'}
-						client.puts send_msg 
-						close_connection(client)
-					end
-				end
-			end
-		end
-	end
-
-	# Get a gpg file
-	# @args: msg -> message puts by the client
-	# @rtrn: json message
-	def get_file(msg)
-		gpg_key = msg['gpg_key'].sub('@', '_')
-
-		if msg['suffix'].nil? || msg['suffix'].empty?
-			file_gpg = "#{@data_dir}/#{gpg_key}.yml"
-		else
-			file_gpg = "#{@data_dir}/#{gpg_key}-#{msg['suffix']}.yml"
-		end
-
-		if File.exist?(file_gpg)
-			gpg_data    = YAML::load_file(file_gpg)
-			salt        = gpg_data['gpg']['salt']
-			hash        = gpg_data['gpg']['hash']
-			data        = gpg_data['gpg']['data']
-
-			if is_authorized?(msg['password'], salt, hash)
-				send_msg = {:action      => 'get',
-				            :gpg_key     => msg['gpg_key'],
-				            :data        => data,
-				            :error       => nil}
-			else
-				send_msg = {:action  => 'get',
-				            :gpg_key => msg['gpg_key'],
-				            :error   => 'server.error.client.no_authorized'}
-			end
-		else
-			send_msg = {:action  => 'get',
-			            :gpg_key => msg['gpg_key'],
-			            :data    => '',
-			            :error   => nil}
-		end
-
-		return send_msg.to_json
-	end
-
-	# Update a file
-	# @args: msg -> message puts by the client
-	# @rtrn: json message
-	def update_file(msg)
-		gpg_key = msg['gpg_key'].sub('@', '_')
-		data    = msg['data']
-
-		if data.nil? || data.empty?
-			send_msg = {:action  => 'update',
-			            :gpg_key => msg['gpg_key'],
-			            :error   => 'server.error.client.no_data'}
-			
-			return send_msg.to_json
-		end
-
-		if msg['suffix'].nil? || msg['suffix'].empty?
-			file_gpg = "#{@data_dir}/#{gpg_key}.yml"
-		else
-			file_gpg = "#{@data_dir}/#{gpg_key}-#{msg['suffix']}.yml"
-		end
-
-		if File.exist?(file_gpg)
-			gpg_data  = YAML::load_file(file_gpg)
-			salt      = gpg_data['gpg']['salt']
-			hash      = gpg_data['gpg']['hash']
-
-		else
-			salt = salt()
-			hash = Digest::SHA256.hexdigest(salt + msg['password'])
-		end
-
-		if is_authorized?(msg['password'], salt, hash)
-			begin
-				config = {'gpg' => {'salt'        => salt,
-				                    'hash'        => hash,
-				                    'data'        => data}}
-
-				File.open(file_gpg, 'w+') do |file|
-					file << config.to_yaml
-				end
-
-				send_msg = {:action  => 'update',
-				            :gpg_key => msg['gpg_key'],
-				            :error   => nil}
-			rescue Exception => e
-				send_msg = {:action  => 'update',
-				            :gpg_key => msg['gpg_key'],
-				            :error   => 'server.error.client.unknown'}
-			end
-		else
-			send_msg = {:action  => 'update',
-			            :gpg_key => msg['gpg_key'],
-			            :error   => 'server.error.client.no_authorized'}
-		end
-		
-		return send_msg.to_json
-	end
-
-	# Remove a gpg file
-	# @args: msg -> message puts by the client
-	# @rtrn: json message
-	def delete_file(msg)
-		gpg_key = msg['gpg_key'].sub('@', '_')
-
-		if msg['suffix'].nil? || msg['suffix'].empty?
-			file_gpg = "#{@data_dir}/#{gpg_key}.yml"
-		else
-			file_gpg = "#{@data_dir}/#{gpg_key}-#{msg['suffix']}.yml"
-		end
-
-		if !File.exist?(file_gpg)
-			send_msg = {:action  => 'delete',
-			            :gpg_key => msg['gpg_key'],
-			            :error   => nil}
-
-			return send_msg.to_json
-		end
-
-		gpg_data  = YAML::load_file(file_gpg)
-		salt      = gpg_data['gpg']['salt']
-		hash      = gpg_data['gpg']['hash']
-
-		if is_authorized?(msg['password'], salt, hash)
-			begin
-				File.unlink(file_gpg)
-
-				send_msg = {:action  => 'delete',
-				            :gpg_key => msg['gpg_key'],
-				            :error   => nil}
-			rescue Exception => e
-				send_msg = {:action  => 'delete',
-				            :gpg_key => msg['gpg_key'],
-				            :error   => 'server.error.client.unknown'}
-			end
-		else
-			send_msg = {:action  => 'delete',
-			            :gpg_key => msg['gpg_key'],
-			            :error   => 'server.error.client.no_authorized'}
-		end
-		
-		return send_msg.to_json
-	end
-
-	# Check is the hash equal the password with the salt
-	# @args: password -> the user password
-	#        salt -> the salt
-	#        hash -> the hash of the password with the salt
-	# @rtrn: true is is good, else false
-	def is_authorized?(password, salt, hash)
-		if hash == Digest::SHA256.hexdigest(salt + password)
-			return true
-		else
-			return false
-		end
-	end
-
-	# Get message to client
-	# @args: client -> client connection
-	# @rtrn: array of the json string, or false if isn't json message
-	def get_client_msg(client)
-		begin
-			msg = client.gets
-			return JSON.parse(msg)
-		rescue
-			closeConnection(client)
-			return false
-		end
-	end
-
-	# Close the client connection
-	# @args: client -> client connection
-	def close_connection(client)
-			client.puts "Closing the connection. Bye!"
-			client.close
-	end
-
-	# Check the config file
-	# @args: file_config -> the configuration file
-	# @rtrn: true if the config file is correct
-	def checkconfig(file_config)
-		begin
-			config    = YAML::load_file(file_config)
-			@host     = config['config']['host']
-			@port     = config['config']['port'].to_i
-			@data_dir = config['config']['data_dir']
-			@log_file = config['config']['log_file']
-			@timeout  = config['config']['timeout'].to_i
-
-			if @host.empty? || @port <= 0 || @data_dir.empty? 
-				puts I18n.t('checkconfig.fail')
-				puts I18n.t('checkconfig.empty')
-				return false
-			end
-
-			if !Dir.exist?(@data_dir)
-				puts I18n.t('checkconfig.fail')
-				puts I18n.t('checkconfig.datadir')
-				return false
-			end
-
-			if @log_file.nil? || @log_file.empty?
-				puts I18n.t('checkconfig.fail')
-				puts I18n.t('checkconfig.log_file_empty')
-				return false
-			else
-				begin
-					@log = Logger.new(@log_file)
-				rescue
-					puts I18n.t('checkconfig.fail')
-					puts I18n.t('checkconfig.log_file_create')
-					return false
-				end
-			end
-
-		rescue Exception => e 
-			puts "#{I18n.t('checkconfig.fail')}\n#{e}"
-			return false
-		end
-
-		return true
-	end
-
-	# Create a new config file
-	# @args: file_config -> the configuration file
-	# @rtrn: true if le config file is create
-	def setup(file_config)
-		puts I18n.t('form.setup.title')
-		puts '--------------------'
-		host     = ask(I18n.t('form.setup.host')).to_s
-		port     = ask(I18n.t('form.setup.port')).to_s
-		data_dir = ask(I18n.t('form.setup.data_dir')).to_s
-		log_file = ask(I18n.t('form.setup.log_file')).to_s
-		timeout  = ask(I18n.t('form.setup.timeout')).to_s
-
-		config = {'config' => {'host'     => host,
-		                       'port'     => port,
-		                       'data_dir' => data_dir,
-		                       'log_file' => log_file,
-		                       'timeout'  => timeout}}
-
-		begin
-			File.open(file_config, 'w') do |file|
-				file << config.to_yaml
-			end
-		rescue Exception => e 
-			puts "#{I18n.t('form.setup.not_valid')}\n#{e}"
-			return false
-		end
-
-		return true
-	end
-
-	# Generate a random salt
-	# @args: length -> the length salt
-	# @rtrn: a random string
-	def salt(length=4)
-		if length.to_i <= 0 || length.to_i > 16
-			length = 4
-		else
-			length = length.to_i
-		end
-
-		return ([*('A'..'Z'),*('a'..'z'),*('0'..'9')]).sample(length).join
-	end
-	
-end
diff --git a/lib/Sync.rb b/lib/Sync.rb
deleted file mode 100644
index e85b0ce..0000000
--- a/lib/Sync.rb
+++ /dev/null
@@ -1,128 +0,0 @@
-#!/usr/bin/ruby
-# author: nishiki
-# mail: nishiki@yaegashi.fr
-# info: a simple script who manage your passwords
-
-require 'rubygems'
-require 'i18n'
-require 'socket'
-require 'json'
-
-require "#{APP_ROOT}/lib/MPW.rb"
-
-class Sync
-
-	attr_accessor :error_msg
-	attr_accessor :enable
-
-	# Constructor
-	def initialize()
-		@error_msg = nil
-		@enable    = false
-	end
-
-	# Connect to server
-	# @args: host -> the server host
-	#        port -> ther connection port
-	#        gpg_key -> the gpg key
-	#        password -> the remote password
-	#        suffix -> the suffix file
-	# @rtrn: false if the connection fail
-	def connect(host, port, gpg_key, password, suffix=nil)
-		@gpg_key  = gpg_key
-		@password = password
-		@suffix   = suffix
-
-		begin
-			@socket = TCPSocket.new(host, port)
-			@enable = true
-		rescue Exception => e
-			@error_msg = "#{I18n.t('error.sync.connection')}\n#{e}"
-			@enable    = false
-		end
-
-		return @enable
-	end
-
-	# Get data on server
-	# @args: gpg_password -> the gpg password
-	# @rtrn: nil if nothing data or error
-	def get(gpg_password)
-		if !@enable
-			return nil
-		end
-
-		send_msg = {:action   => 'get',
-		            :gpg_key  => @gpg_key,
-		            :password => @password,
-		            :suffix   => @suffix}
-		
-		@socket.puts send_msg.to_json
-		msg = JSON.parse(@socket.gets)
-
-		if !defined?(msg['error'])
-			@error_msg = I18n.t('error.sync.communication')
-			return nil
-		elsif msg['error'].nil?
-			tmp_file = "/tmp/mpw-#{MPW.password()}.gpg"
-			File.open(tmp_file, 'w') do |file|
-				file << msg['data']
-			end
-			
-			@mpw = MPW.new(tmp_file)
-			if !@mpw.decrypt(gpg_password)
-				puts @mpw.error_msg
-				return nil
-			end
-
-			File.unlink(tmp_file)
-			return @mpw.search()
-		else
-			@error_msg = I18n.t(msg['error'])
-			return nil
-		end
-
-	end
-
-	# Update the remote data
-	# @args: data -> the data to send on server
-	# @rtrn: false if there is a problem
-	def update(data)
-		if !@enable
-			return true
-		end
-
-		send_msg = {:action   => 'update',
-		            :gpg_key  => @gpg_key,
-		            :password => @password,
-		            :suffix   => @suffix,
-		            :data     => data}
-		
-		@socket.puts send_msg.to_json
-		msg = JSON.parse(@socket.gets)
-
-		if !defined?(msg['error'])
-			@error_msg = I18n.t('error.sync.communication')
-			return false
-		elsif msg['error'].nil?
-			return true
-		else
-			@error_msg = I18n.t(msg['error'])
-			return false
-		end
-	end
-
-	# TODO
-	def delete()
-	end
-
-	# Close the connection
-	def close()
-		if !@enable
-			return
-		end
-
-		send_msg = {:action => 'close'}
-		@socket.puts send_msg.to_json
-	end
-end
diff --git a/lib/mpw/cli.rb b/lib/mpw/cli.rb
new file mode 100644
index 0000000..e64d27c
--- /dev/null
+++ b/lib/mpw/cli.rb
@@ -0,0 +1,623 @@
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'readline'
+require 'locale'
+require 'i18n'
+require 'colorize'
+require 'highline/import'
+require 'clipboard'
+require 'tmpdir'
+require 'mpw/item'
+require 'mpw/mpw'
+
+module MPW
+  class Cli
+    # @param config [Config]
+    def initialize(config)
+      @config = config
+    end
+
+    # Change a parameter int the config after init
+    # @param options [Hash] param to change
+    def set_config(options)
+      @config.setup(options)
+
+      puts I18n.t('form.set_config.valid').to_s.green
+    rescue => e
+      puts "#{I18n.t('display.error')} #15: #{e}".red
+      exit 2
+    end
+
+    # Change the wallet path
+    # @param path [String] new path
+    def set_wallet_path(path)
+      @config.set_wallet_path(path, @wallet)
+
+      puts I18n.t('form.set_wallet_path.valid').to_s.green
+    rescue => e
+      puts "#{I18n.t('display.error')} #19: #{e}".red
+      exit 2
+    end
+
+    # Create a new config file
+    # @param options [Hash]
+    def setup(options)
+      options[:lang] = options[:lang] || Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1]
+
+      I18n.locale = options[:lang].to_sym
+
+      @config.setup(options)
+
+      load_config
+
+      puts I18n.t('form.setup_config.valid').to_s.green
+    rescue => e
+      puts "#{I18n.t('display.error')} #8: #{e}".red
+      exit 2
+    end
+
+    # Setup a new GPG key
+    # @param gpg_key [String] gpg key name
+    def setup_gpg_key(gpg_key)
+      return if @config.check_gpg_key?
+
+      password = ask(I18n.t('form.setup_gpg_key.password')) { |q| q.echo = false }
+      confirm  = ask(I18n.t('form.setup_gpg_key.confirm_password')) { |q| q.echo = false }
+
+      raise I18n.t('form.setup_gpg_key.error_password') if password != confirm
+
+      @password = password.to_s
+
+      puts I18n.t('form.setup_gpg_key.wait')
+
+      @config.setup_gpg_key(@password, gpg_key)
+
+      puts I18n.t('form.setup_gpg_key.valid').to_s.green
+    rescue => e
+      puts "#{I18n.t('display.error')} #8: #{e}".red
+      exit 2
+    end
+
+    # List gpg keys in wallet
+    def list_keys
+      table_list('keys', @mpw.list_keys)
+    end
+
+    # List config
+    def list_config
+      config = {
+        'lang'           => @config.lang,
+        'gpg_key'        => @config.gpg_key,
+        'default_wallet' => @config.default_wallet,
+        'wallet_dir'     => @config.wallet_dir,
+        'pinmode'        => @config.pinmode,
+        'gpg_exe'        => @config.gpg_exe
+      }
+
+      @config.wallet_paths.each { |k, v| config["path_wallet_#{k}"] = "#{v}/#{k}.mpw" }
+      @config.password.each     { |k, v| config["password_#{k}"] = v }
+
+      table_list('config', config)
+    end
+
+    # Load config
+    def load_config
+      @config.load_config
+    rescue => e
+      puts "#{I18n.t('display.error')} #10: #{e}".red
+      exit 2
+    end
+
+    # Request the GPG password and decrypt the file
+    def decrypt
+      if defined?(@mpw)
+        @mpw.read_data
+      else
+        begin
+          @mpw = MPW.new(@config.gpg_key, @wallet_file, nil, @config.gpg_exe, @config.pinmode)
+
+          @mpw.read_data
+        rescue
+          @password = ask(I18n.t('display.gpg_password')) { |q| q.echo = false }
+          @mpw      = MPW.new(@config.gpg_key, @wallet_file, @password, @config.gpg_exe, @config.pinmode)
+
+          @mpw.read_data
+        end
+      end
+    rescue => e
+      puts "#{I18n.t('display.error')} #11: #{e}".red
+      exit 2
+    end
+
+    # Format list on a table
+    # @param title [String] name of table
+    # @param list  an array or hash
+    def table_list(title, list)
+      length = { k: 0, v: 0 }
+
+      if list.is_a?(Array)
+        i    = 0
+        list = list.map do |item|
+          i += 1
+          [i, item]
+        end.to_h
+      end
+
+      list.each do |k, v|
+        length[:k] = k.to_s.length if length[:k] < k.to_s.length
+        length[:v] = v.to_s.length if length[:v] < v.to_s.length
+      end
+
+      puts "\n#{I18n.t("display.#{title}")}".red
+      print ' '
+      (length[:k] + length[:v] + 5).times { print '=' }
+      print "\n"
+
+      list.each do |k, v|
+        print "  #{k}".cyan
+        (length[:k] - k.to_s.length + 1).times { print ' ' }
+        puts "| #{v}"
+      end
+
+      print "\n"
+    end
+
+    # Format items on a table
+    # @param items [Array]
+    def table_items(items = [])
+      group        = '.'
+      i            = 1
+      length_total = 10
+      data         = { id:       { length: 3,  color: 'cyan' },
+                       host:     { length: 9,  color: 'yellow' },
+                       user:     { length: 7,  color: 'green' },
+                       otp:      { length: 4,  color: 'white' },
+                       comment:  { length: 14, color: 'magenta' } }
+
+      items.each do |item|
+        data.each do |k, v|
+          case k
+          when :id, :otp
+            next
+          when :host
+            v[:length] = item.url.length + 3 if item.url.length >= v[:length]
+          else
+            v[:length] = item.send(k.to_s).to_s.length + 3 if item.send(k.to_s).to_s.length >= v[:length]
+          end
+        end
+      end
+      data[:id][:length] = items.length.to_s.length + 2 if items.length.to_s.length > data[:id][:length]
+
+      data.each_value { |v| length_total += v[:length] }
+      items.sort!     { |a, b| a.group.to_s.downcase <=> b.group.to_s.downcase }
+
+      items.each do |item|
+        if group != item.group
+          group = item.group
+
+          if group.to_s.empty?
+            puts "\n#{I18n.t('display.no_group')}".red
+          else
+            puts "\n#{group}".red
+          end
+
+          print ' '
+          length_total.times { print '=' }
+          print "\n "
+          data.each do |k, v|
+            case k
+            when :id
+              print ' ID'
+            when :otp
+              print '| OTP'
+            else
+              print "| #{k.to_s.capitalize}"
+            end
+
+            (v[:length] - k.to_s.length).times { print ' ' }
+          end
+          print "\n "
+          length_total.times { print '=' }
+          print "\n"
+        end
+
+        print "  #{i}".send(data[:id][:color])
+        (data[:id][:length] - i.to_s.length).times { print ' ' }
+        data.each do |k, v|
+          next if k == :id
+
+          print '| '
+
+          case k
+          when :otp
+            item.otp ? (print ' X  ') : 4.times { print ' ' }
+
+          when :host
+            print "#{item.protocol}://".light_black if item.protocol
+            print item.host.send(v[:color])
+            print ":#{item.port}".light_black if item.port
+            (v[:length] - item.url.to_s.length).times { print ' ' }
+
+          else
+            print item.send(k.to_s).to_s.send(v[:color])
+            (v[:length] - item.send(k.to_s).to_s.length).times { print ' ' }
+          end
+        end
+        print "\n"
+
+        i += 1
+      end
+
+      print "\n"
+    end
+
+    # Display the query's result
+    # @param options [Hash] the options to search
+    def list(**options)
+      result = @mpw.list(options)
+
+      if result.empty?
+        puts I18n.t('display.nothing')
+      else
+        table_items(result)
+      end
+    end
+
+    # Get an item when multiple choice
+    # @param items [Array] list of items
+    # @return [Item] an item
+    def get_item(items)
+      return items[0] if items.length == 1
+
+      items.sort! { |a, b| a.group.to_s.downcase <=> b.group.to_s.downcase }
+      choice = ask(I18n.t('form.select.choice')).to_i
+
+      raise I18n.t('form.select.error') unless choice >= 1 && choice <= items.length
+
+      items[choice - 1]
+    end
+
+    # Print help message for clipboard mode
+    # @param item [Item]
+    def clipboard_help(item)
+      puts "----- #{I18n.t('form.clipboard.help.name')} -----".cyan
+      puts I18n.t('form.clipboard.help.url')
+      puts I18n.t('form.clipboard.help.login')
+      puts I18n.t('form.clipboard.help.password')
+      puts I18n.t('form.clipboard.help.otp_code') if item.otp
+      puts I18n.t('form.clipboard.help.quit')
+    end
+
+    # Copy in clipboard the login and password
+    # @param item [Item]
+    # @param clipboard [Boolean] enable clipboard
+    def clipboard(item, clipboard = true)
+      # Security: force quit after 90s
+      Thread.new do
+        sleep 90
+        exit
+      end
+
+      Kernel.loop do
+        choice = ask(I18n.t('form.clipboard.choice')).to_s
+
+        case choice
+        when 'q', 'quit'
+          break
+
+        when 'u', 'url'
+          if clipboard
+            Clipboard.copy(item.url)
+            puts I18n.t('form.clipboard.url').green
+          else
+            puts item.url
+          end
+
+        when 'l', 'login'
+          if clipboard
+            Clipboard.copy(item.user)
+            puts I18n.t('form.clipboard.login').green
+          else
+            puts item.user
+          end
+
+        when 'p', 'password'
+          if clipboard
+            Clipboard.copy(@mpw.get_password(item.id))
+            puts I18n.t('form.clipboard.password').yellow
+
+            Thread.new do
+              sleep 30
+
+              Clipboard.clear
+            end
+          else
+            puts @mpw.get_password(item.id)
+          end
+
+        when 'o', 'otp'
+          if !item.otp
+            clipboard_help(item)
+            next
+          elsif clipboard
+            Clipboard.copy(@mpw.get_otp_code(item.id))
+          else
+            puts @mpw.get_otp_code(item.id)
+          end
+          puts I18n.t('form.clipboard.otp', time: @mpw.get_otp_remaining_time).yellow
+
+        else
+          clipboard_help(item)
+        end
+      end
+
+      Clipboard.clear
+    rescue SystemExit, Interrupt
+      Clipboard.clear
+    end
+
+    # List all wallets
+    def list_wallet
+      wallets = @config.wallet_paths.keys
+
+      Dir.glob("#{@config.wallet_dir}/*.mpw").each do |f|
+        wallet = File.basename(f, '.mpw')
+        wallet += ' *'.green if wallet == @config.default_wallet
+        wallets << wallet
+      end
+
+      table_list('wallets', wallets)
+    end
+
+    # Display the wallet
+    # @param wallet [String] wallet name
+    def get_wallet(wallet = nil)
+      @wallet =
+        if wallet.to_s.empty?
+          wallets = Dir.glob("#{@config.wallet_dir}/*.mpw")
+          if wallets.length == 1
+            File.basename(wallets[0], '.mpw')
+          elsif !@config.default_wallet.to_s.empty?
+            @config.default_wallet
+          else
+            'default'
+          end
+        else
+          wallet
+        end
+
+      @wallet_file =
+        if @config.wallet_paths.key?(@wallet)
+          "#{@config.wallet_paths[@wallet]}/#{@wallet}.mpw"
+        else
+          "#{@config.wallet_dir}/#{@wallet}.mpw"
+        end
+    end
+
+    # Add a new public key
+    # @param key [String] key name or key file to add
+    def add_key(key)
+      @mpw.add_key(key)
+      @mpw.write_data
+
+      puts I18n.t('form.add_key.valid').to_s.green
+    rescue => e
+      puts "#{I18n.t('display.error')} #13: #{e}".red
+    end
+
+    # Add new public key
+    # @param key [String] key name to delete
+    def delete_key(key)
+      @mpw.delete_key(key)
+      @mpw.write_data
+
+      puts I18n.t('form.delete_key.valid').to_s.green
+    rescue => e
+      puts "#{I18n.t('display.error')} #15: #{e}".red
+    end
+
+    # Text editor interface
+    # @param template_name [String] template name
+    # @param item [Item] the item to edit
+    # @param password [Boolean] disable field password
+    # @return [Hash] the values for an item
+    def text_editor(template_name, password = false, item = nil, **options)
+      editor        = ENV['EDITOR'] || 'nano'
+      opts          = {}
+      template_file = "#{File.expand_path('../../../templates', __FILE__)}/#{template_name}.erb"
+      template      = ERB.new(IO.read(template_file))
+
+      Dir.mktmpdir do |dir|
+        tmp_file = "#{dir}/#{template_name}.yml"
+
+        File.open(tmp_file, 'w') do |f|
+          f << template.result(binding)
+        end
+
+        system("#{editor} #{tmp_file}")
+
+        opts = YAML.load_file(tmp_file)
+      end
+
+      opts.delete_if { |_, v| v.to_s.empty? }
+
+      opts.each do |k, v|
+        options[k.to_sym] = v
+      end
+
+      options
+    end
+
+    # Form to add a new item
+    # @param password [Boolean] generate a random password
+    # @param text_editor [Boolean] enable text editor mode
+    # @param values [Hash] multiples value to set the item
+    def add(password = false, text_editor = false, **values)
+      options            = text_editor('add_form', password, nil, values) if text_editor
+      item               = Item.new(options)
+      options[:password] = MPW.password(@config.password) if password
+
+      @mpw.add(item)
+      @mpw.set_password(item.id, options[:password]) if options.key?(:password)
+      @mpw.set_otp_key(item.id, options[:otp_key])   if options.key?(:otp_key)
+      @mpw.write_data
+
+      puts I18n.t('form.add_item.valid').to_s.green
+    rescue => e
+      puts "#{I18n.t('display.error')} #13: #{e}".red
+    end
+
+    # Update an item
+    # @param password [Boolean] generate a random password
+    # @param text_editor [Boolean] enable text editor mode
+    # @param options [Hash] the options to search
+    # @param values [Hash] multiples value to set the item
+    def update(password = false, text_editor = false, options = {}, **values)
+      items = @mpw.list(options)
+
+      if items.empty?
+        puts I18n.t('display.nothing')
+      else
+        table_items(items) if items.length > 1
+
+        item              = get_item(items)
+        values            = text_editor('update_form', password, item, values) if text_editor
+        values[:password] = MPW.password(@config.password) if password
+
+        item.update(values)
+        @mpw.set_password(item.id, values[:password]) if values.key?(:password)
+        @mpw.set_otp_key(item.id, values[:otp_key])   if values.key?(:otp_key)
+        @mpw.write_data
+
+        puts I18n.t('form.update_item.valid').to_s.green
+      end
+    rescue => e
+      puts "#{I18n.t('display.error')} #14: #{e}".red
+    end
+
+    # Remove an item
+    # @param options [Hash] the options to search
+    def delete(**options)
+      items = @mpw.list(options)
+
+      if items.empty?
+        puts I18n.t('display.nothing')
+      else
+        table_items(items)
+
+        item    = get_item(items)
+        confirm = ask("#{I18n.t('form.delete_item.ask')} (y/N) ").to_s
+
+        return unless confirm =~ /^(y|yes|YES|Yes|Y)$/
+
+        item.delete
+        @mpw.write_data
+
+        puts I18n.t('form.delete_item.valid').to_s.green
+      end
+    rescue => e
+      puts "#{I18n.t('display.error')} #16: #{e}".red
+    end
+
+    # Copy a password, otp, login
+    # @param clipboard [Boolean] enable clipboard
+    # @param options [Hash] the options to search
+    def copy(clipboard = true, **options)
+      items = @mpw.list(options)
+
+      if items.empty?
+        puts I18n.t('display.nothing')
+      else
+        table_items(items)
+
+        item = get_item(items)
+        clipboard(item, clipboard)
+      end
+    rescue => e
+      puts "#{I18n.t('display.error')} #14: #{e}".red
+    end
+
+    # Export the items in an yaml file
+    # @param file [String] the path of destination file
+    # @param options [Hash] options to search
+    def export(file, options)
+      file  = 'export-mpw.yml' if file.to_s.empty?
+      items = @mpw.list(options)
+      data  = {}
+
+      items.each do |item|
+        data.merge!(
+          item.id => {
+            'comment'   => item.comment,
+            'created'   => item.created,
+            'group'     => item.group,
+            'last_edit' => item.last_edit,
+            'otp_key'   => @mpw.get_otp_key(item.id),
+            'password'  => @mpw.get_password(item.id),
+            'url'       => item.url,
+            'user'      => item.user
+          }
+        )
+      end
+
+      File.open(file, 'w') { |f| f << data.to_yaml }
+
+      puts I18n.t('form.export.valid', file: file).to_s.green
+    rescue => e
+      puts "#{I18n.t('display.error')} #17: #{e}".red
+    end
+
+    # Import items from an yaml file
+    # @param file [String] path of import file
+    # @param format [String] the software import file format
+    def import(file, format = 'mpw')
+      raise I18n.t('form.import.file_empty')     if file.to_s.empty?
+      raise I18n.t('form.import.file_not_exist') unless File.exist?(file)
+
+      begin
+        require "mpw/import/#{format}"
+      rescue LoadError
+        raise I18n.t('form.import.format_unknown', file_format: format)
+      end
+
+      Import.send(format, file).each_value do |row|
+        item = Item.new(
+          comment:  row['comment'],
+          group:    row['group'],
+          url:      row['url'],
+          user:     row['user']
+        )
+
+        next if item.empty?
+
+        @mpw.add(item)
+        @mpw.set_password(item.id, row['password']) unless row['password'].to_s.empty?
+        @mpw.set_otp_key(item.id, row['otp_key'])   unless row['otp_key'].to_s.empty?
+      end
+
+      @mpw.write_data
+
+      puts I18n.t('form.import.valid').to_s.green
+    rescue => e
+      puts "#{I18n.t('display.error')} #18: #{e}".red
+    end
+  end
+end
diff --git a/lib/mpw/config.rb b/lib/mpw/config.rb
new file mode 100644
index 0000000..f3974b5
--- /dev/null
+++ b/lib/mpw/config.rb
@@ -0,0 +1,190 @@
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'gpgme'
+require 'yaml'
+require 'i18n'
+require 'fileutils'
+
+module MPW
+  class Config
+    attr_accessor :error_msg
+
+    attr_accessor :gpg_key
+    attr_accessor :lang
+    attr_accessor :config_dir
+    attr_accessor :default_wallet
+    attr_accessor :wallet_dir
+    attr_accessor :wallet_paths
+    attr_accessor :gpg_exe
+    attr_accessor :password
+    attr_accessor :pinmode
+
+    # @param config_file [String] path of config file
+    def initialize(config_file = nil)
+      @config_file = config_file
+      @config_dir  =
+        if RUBY_PLATFORM =~ /darwin/
+          "#{Dir.home}/Library/Preferences/mpw"
+        elsif RUBY_PLATFORM =~ /cygwin|mswin|mingw|bccwin|wince|emx/
+          "#{Dir.home}/AppData/Local/mpw"
+        else
+          "#{Dir.home}/.config/mpw"
+        end
+
+      @config_file = "#{@config_dir}/mpw.cfg" if @config_file.to_s.empty?
+    end
+
+    # Create a new config file
+    # @param options [Hash] the value to set the config file
+    def setup(**options)
+      gpg_key        = options[:gpg_key]        || @gpg_key
+      lang           = options[:lang]           || @lang
+      wallet_dir     = options[:wallet_dir]     || @wallet_dir
+      default_wallet = options[:default_wallet] || @default_wallet
+      gpg_exe        = options[:gpg_exe]        || @gpg_exe
+      pinmode        = options.key?(:pinmode) ? options[:pinmode] : @pinmode
+      password       = {
+        numeric: true,
+        alpha:   true,
+        special: false,
+        length:  16
+      }
+
+      %w[numeric special alpha length].each do |k|
+        if options.key?("pwd_#{k}".to_sym)
+          password[k.to_sym] = options["pwd_#{k}".to_sym]
+        elsif !@password.nil? && @password.key?(k.to_sym)
+          password[k.to_sym] = @password[k.to_sym]
+        end
+      end
+
+      unless gpg_key =~ /[a-zA-Z0-9.-_]+\@[a-zA-Z0-9]+\.[a-zA-Z]+/
+        raise I18n.t('error.config.key_bad_format')
+      end
+
+      wallet_dir = "#{@config_dir}/wallets" if wallet_dir.to_s.empty?
+      config     = { 'gpg_key'        => gpg_key,
+                     'lang'           => lang,
+                     'wallet_dir'     => wallet_dir,
+                     'default_wallet' => default_wallet,
+                     'gpg_exe'        => gpg_exe,
+                     'password'       => password,
+                     'pinmode'        => pinmode,
+                     'wallet_paths'   => @wallet_paths }
+
+      FileUtils.mkdir_p(@config_dir, mode: 0700)
+      FileUtils.mkdir_p(wallet_dir,  mode: 0700)
+
+      File.open(@config_file, 'w') do |file|
+        file << config.to_yaml
+      end
+    rescue => e
+      raise "#{I18n.t('error.config.write')}\n#{e}"
+    end
+
+    # Setup a new gpg key
+    # @param password [String] gpg key password
+    # @param name [String] the name of user
+    # @param length [Integer] length of the gpg key
+    # @param expire [Integer] time of expire to gpg key
+    def setup_gpg_key(password, name, length = 4096, expire = 0)
+      raise I18n.t('error.config.genkey_gpg.name') if name.to_s.empty?
+      raise I18n.t('error.config.genkey_gpg.password') if password.to_s.empty?
+
+      param = ''
+      param << '<GnupgKeyParms format="internal">' + "\n"
+      param << "Key-Type: RSA\n"
+      param << "Key-Length: #{length}\n"
+      param << "Subkey-Type: ELG-E\n"
+      param << "Subkey-Length: #{length}\n"
+      param << "Name-Real: #{name}\n"
+      param << "Name-Comment: #{name}\n"
+      param << "Name-Email: #{@gpg_key}\n"
+      param << "Expire-Date: #{expire}\n"
+      param << "Passphrase: #{password}\n"
+      param << "</GnupgKeyParms>\n"
+
+      ctx = GPGME::Ctx.new
+      ctx.genkey(param, nil, nil)
+    rescue => e
+      raise "#{I18n.t('error.config.genkey_gpg.exception')}\n#{e}"
+    end
+
+    # Load the config file
+    def load_config
+      config          = YAML.load_file(@config_file)
+      @gpg_key        = config['gpg_key']
+      @lang           = config['lang']
+      @wallet_dir     = config['wallet_dir']
+      @wallet_paths   = config['wallet_paths'] || {}
+      @default_wallet = config['default_wallet']
+      @gpg_exe        = config['gpg_exe']
+      @password       = config['password'] || {}
+      @pinmode        = config['pinmode'] || false
+
+      raise if @gpg_key.empty? || @wallet_dir.empty?
+
+      I18n.locale = @lang.to_sym
+    rescue => e
+      raise "#{I18n.t('error.config.load')}\n#{e}"
+    end
+
+    # Check if private key exist
+    # @return [Boolean] true if the key exist, else false
+    def check_gpg_key?
+      ctx = GPGME::Ctx.new
+      ctx.each_key(@gpg_key, true) do
+        return true
+      end
+
+      false
+    end
+
+    # Change the path of one wallet
+    # @param path [String]new directory path
+    # @param wallet [String] wallet name
+    def set_wallet_path(path, wallet)
+      path = @wallet_dir if path == 'default'
+      path = File.absolute_path(path)
+
+      return if path == @wallet_dir && File.exist?("#{@wallet_dir}/#{wallet}.mpw")
+      return if path == @wallet_paths[wallet]
+
+      old_wallet_file =
+        if @wallet_paths.key?(wallet)
+          "#{@wallet_paths[wallet]}/#{wallet}.mpw"
+        else
+          "#{@wallet_dir}/#{wallet}.mpw"
+        end
+
+      FileUtils.mkdir_p(path) unless Dir.exist?(path)
+      FileUtils.mv(old_wallet_file, "#{path}/#{wallet}.mpw") if File.exist?(old_wallet_file)
+
+      if path == @wallet_dir
+        @wallet_paths.delete(wallet)
+      else
+        @wallet_paths[wallet] = path
+      end
+
+      setup
+    end
+  end
+end
diff --git a/lib/mpw/import/gorilla.rb b/lib/mpw/import/gorilla.rb
new file mode 100644
index 0000000..feffce6
--- /dev/null
+++ b/lib/mpw/import/gorilla.rb
@@ -0,0 +1,53 @@
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'csv'
+
+module MPW
+  module Import
+    # Import an export mpw file
+    # @param file [String] the file path to import
+    def self.gorilla(file)
+      data = {}
+
+      CSV.foreach(file, headers: true) do |row|
+        id = row['uuid']
+        comment =
+          if row['title'] && row['notes']
+            "#{row['title']} #{row['notes']}"
+          elsif row['title']
+            row['title']
+          elsif row['notes']
+            row['notes']
+          end
+
+        data[id] = {
+          'comment'  => comment,
+          'group'    => row['group'],
+          'password' => row['password'],
+          'url'      => row['url'],
+          'user'     => row['user']
+        }
+      end
+
+      data
+    end
+  end
+end
diff --git a/lib/mpw/import/keepass.rb b/lib/mpw/import/keepass.rb
new file mode 100644
index 0000000..0b961d4
--- /dev/null
+++ b/lib/mpw/import/keepass.rb
@@ -0,0 +1,53 @@
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'csv'
+
+module MPW
+  module Import
+    # Import an keepass2 export csv file
+    # @param file [String] the file path to import
+    def self.keepass(file)
+      data = {}
+
+      CSV.foreach(file, headers: true) do |row|
+        id = "#{row['Group']} #{row['Title']}"
+        comment =
+          if row['Title'] && row['Notes']
+            "#{row['Title']} #{row['Notes']}"
+          elsif row['Title']
+            row['Title']
+          elsif row['Notes']
+            row['Notes']
+          end
+
+        data[id] = {
+          'comment'  => comment,
+          'group'    => row['Group'],
+          'password' => row['Password'],
+          'url'      => row['URL'],
+          'user'     => row['Username']
+        }
+      end
+
+      data
+    end
+  end
+end
diff --git a/lib/mpw/import/mpw.rb b/lib/mpw/import/mpw.rb
new file mode 100644
index 0000000..a287357
--- /dev/null
+++ b/lib/mpw/import/mpw.rb
@@ -0,0 +1,31 @@
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'yaml'
+
+module MPW
+  module Import
+    # Import an export mpw file
+    # @param file [String] the file path to import
+    def self.mpw(file)
+      YAML.load_file(file)
+    end
+  end
+end
diff --git a/lib/mpw/import/mpw_old.rb b/lib/mpw/import/mpw_old.rb
new file mode 100644
index 0000000..923bf16
--- /dev/null
+++ b/lib/mpw/import/mpw_old.rb
@@ -0,0 +1,48 @@
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'yaml'
+
+module MPW
+  module Import
+    # Import an export mpw file
+    # @param file [String] the file path to import
+    def self.mpw_old(file)
+      data = {}
+      YAML.load_file(file).each do |id, item|
+        url = ''
+        url += "#{item['protocol']}://" if item['protocol']
+        url += item['host']
+        url += ":#{item['port']}" if item['port']
+
+        data[id] = {
+          'comment'  => item['comment'],
+          'group'    => item['group'],
+          'otp'      => item['otp'],
+          'password' => item['password'],
+          'url'      => url,
+          'user'     => item['user']
+        }
+      end
+
+      data
+    end
+  end
+end
diff --git a/lib/mpw/item.rb b/lib/mpw/item.rb
new file mode 100644
index 0000000..9da60b1
--- /dev/null
+++ b/lib/mpw/item.rb
@@ -0,0 +1,108 @@
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'i18n'
+require 'uri'
+
+module MPW
+  class Item
+    attr_accessor :created
+    attr_accessor :comment
+    attr_accessor :group
+    attr_accessor :host
+    attr_accessor :id
+    attr_accessor :otp
+    attr_accessor :port
+    attr_accessor :protocol
+    attr_accessor :last_edit
+    attr_accessor :url
+    attr_accessor :user
+
+    # @param options [Hash] the option :host is required
+    def initialize(**options)
+      @host = ''
+
+      if !options[:id] || !options[:created]
+        @id = generate_id
+        @created = Time.now.to_i
+      else
+        @id = options[:id]
+        @created   = options[:created]
+        @last_edit = options[:last_edit]
+        options[:no_update_last_edit] = true
+      end
+
+      update(options)
+    end
+
+    # Update the item
+    # @param options [Hash]
+    def update(**options)
+      unless options[:url] || options[:comment]
+        raise I18n.t('error.update.host_and_comment_empty')
+      end
+
+      if options[:url]
+        uri       = URI(options[:url])
+        @host     = uri.host   || options[:url]
+        @port     = uri.port   || nil
+        @protocol = uri.scheme || nil
+        @url      = options[:url]
+      end
+
+      @comment   = options[:comment] if options.key?(:comment)
+      @group     = options[:group]   if options.key?(:group)
+      @last_edit = Time.now.to_i     unless options.key?(:no_update_last_edit)
+      @otp       = options[:otp]     if options.key?(:otp)
+      @user      = options[:user]    if options.key?(:user)
+    end
+
+    # Delete all data
+    def delete
+      @id        = nil
+      @comment   = nil
+      @created   = nil
+      @group     = nil
+      @host      = nil
+      @last_edit = nil
+      @otp       = nil
+      @port      = nil
+      @protocol  = nil
+      @url       = nil
+      @user      = nil
+    end
+
+    def empty?
+      @id.to_s.empty?
+    end
+
+    def nil?
+      false
+    end
+
+    private
+
+    # Generate an random id
+    # @return [String] random string
+    def generate_id
+      [*('A'..'Z'), *('a'..'z'), *('0'..'9')].sample(16).join
+    end
+  end
+end
diff --git a/lib/mpw/mpw.rb b/lib/mpw/mpw.rb
new file mode 100644
index 0000000..191caae
--- /dev/null
+++ b/lib/mpw/mpw.rb
@@ -0,0 +1,351 @@
+#
+# Copyright:: 2013, Adrien Waksberg
+#
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+require 'rubygems/package'
+require 'gpgme'
+require 'i18n'
+require 'yaml'
+require 'rotp'
+require 'mpw/item'
+
+module MPW
+  class MPW
+    # @param key [String] gpg key name
+    # @param wallet_file [String] path of the wallet file
+    # @param gpg_pass [String] password of the gpg key
+    # @param gpg_exe [String] path of the gpg executable
+    # @param pinmode [Boolean] enable the gpg pinmode
+    def initialize(key, wallet_file, gpg_pass = nil, gpg_exe = nil, pinmode = false)
+      @key         = key
+      @gpg_pass    = gpg_pass
+      @gpg_exe     = gpg_exe
+      @wallet_file = wallet_file
+      @pinmode     = pinmode
+
+      GPGME::Engine.set_info(GPGME::PROTOCOL_OpenPGP, @gpg_exe, "#{Dir.home}/.gnupg") unless @gpg_exe.to_s.empty?
+    end
+
+    # Read mpw file
+    def read_data
+      @data      = []
+      @keys      = {}
+      @passwords = {}
+      @otp_keys  = {}
+
+      data       = nil
+
+      return unless File.exist?(@wallet_file)
+
+      Gem::Package::TarReader.new(File.open(@wallet_file)) do |tar|
+        tar.each do |f|
+          case f.full_name
+          when 'wallet/meta.gpg'
+            data = decrypt(f.read)
+
+          when %r{^wallet/keys/(?<key>.+)\.pub$}
+            key = Regexp.last_match('key')
+
+            if GPGME::Key.find(:public, key).empty?
+              GPGME::Key.import(f.read, armor: true)
+            end
+
+            @keys[key] = f.read
+
+          when %r{^wallet/passwords/(?<id>[a-zA-Z0-9]+)\.gpg$}
+            @passwords[Regexp.last_match('id')] = f.read
+
+          when %r{^wallet/otp_keys/(?<id>[a-zA-Z0-9]+)\.gpg$}
+            @otp_keys[Regexp.last_match('id')] = f.read
+
+          else
+            next
+          end
+        end
+      end
+
+      unless data.to_s.empty?
+        YAML.safe_load(data).each_value do |d|
+          @data.push(
+            Item.new(
+              id:        d['id'],
+              group:     d['group'],
+              user:      d['user'],
+              url:       d['url'],
+              otp:       @otp_keys.key?(d['id']),
+              comment:   d['comment'],
+              last_edit: d['last_edit'],
+              created:   d['created']
+            )
+          )
+        end
+      end
+
+      add_key(@key) unless @keys.key?(@key)
+    rescue => e
+      raise "#{I18n.t('error.mpw_file.read_data')}\n#{e}"
+    end
+
+    # Encrypt all data in tarball
+    def write_data
+      data     = {}
+      tmp_file = "#{@wallet_file}.tmp"
+
+      @data.each do |item|
+        next if item.empty?
+
+        data.merge!(
+          item.id => {
+            'id'        => item.id,
+            'group'     => item.group,
+            'user'      => item.user,
+            'url'       => item.url,
+            'comment'   => item.comment,
+            'last_edit' => item.last_edit,
+            'created'   => item.created
+          }
+        )
+      end
+
+      Gem::Package::TarWriter.new(File.open(tmp_file, 'w+')) do |tar|
+        data_encrypt = encrypt(data.to_yaml)
+        tar.add_file_simple('wallet/meta.gpg', 0400, data_encrypt.length) do |io|
+          io.write(data_encrypt)
+        end
+
+        @passwords.each do |id, password|
+          tar.add_file_simple("wallet/passwords/#{id}.gpg", 0400, password.length) do |io|
+            io.write(password)
+          end
+        end
+
+        @otp_keys.each do |id, key|
+          tar.add_file_simple("wallet/otp_keys/#{id}.gpg", 0400, key.length) do |io|
+            io.write(key)
+          end
+        end
+
+        @keys.each do |id, key|
+          tar.add_file_simple("wallet/keys/#{id}.pub", 0400, key.length) do |io|
+            io.write(key)
+          end
+        end
+      end
+
+      File.rename(tmp_file, @wallet_file)
+    rescue => e
+      File.unlink(tmp_file) if File.exist?(tmp_file)
+
+      raise "#{I18n.t('error.mpw_file.write_data')}\n#{e}"
+    end
+
+    # Get a password
+    # @param id [String] the item id
+    def get_password(id)
+      password = decrypt(@passwords[id])
+
+      if /^\$[a-zA-Z0-9]{4,9}::(?<password>.+)$/ =~ password
+        Regexp.last_match('password')
+      else
+        password
+      end
+    end
+
+    # Set a new password for an item
+    # @param id [String] the item id
+    # @param password [String] the new password
+    def set_password(id, password)
+      salt     = MPW.password(length: Random.rand(4..9))
+      password = "$#{salt}::#{password}"
+
+      @passwords[id] = encrypt(password)
+    end
+
+    # Return the list of all gpg keys
+    # @return [Array] the gpg keys name
+    def list_keys
+      @keys.keys
+    end
+
+    # Add a public key
+    # @param key [String] new public key file or name
+    def add_key(key)
+      if File.exist?(key)
+        data       = File.open(key).read
+        key_import = GPGME::Key.import(data, armor: true)
+        key        = GPGME::Key.get(key_import.imports[0].fpr).uids[0].email
+      else
+        data = GPGME::Key.export(key, armor: true).read
+      end
+
+      raise I18n.t('error.export_key') if data.to_s.empty?
+
+      @keys[key] = data
+      @passwords.each_key { |id| set_password(id, get_password(id)) }
+      @otp_keys.each_key { |id| set_otp_key(id, get_otp_key(id)) }
+    end
+
+    # Delete a public key
+    # @param key [String] public key to delete
+    def delete_key(key)
+      @keys.delete(key)
+      @passwords.each_key { |id| set_password(id, get_password(id)) }
+      @otp_keys.each_key { |id| set_otp_key(id, get_otp_key(id)) }
+    end
+
+    # Add a new item
+    # @param item [Item]
+    def add(item)
+      raise I18n.t('error.bad_class') unless item.instance_of?(Item)
+      raise I18n.t('error.empty')     if item.empty?
+
+      @data.push(item)
+    end
+
+    # Search in some csv data
+    # @param options [Hash]
+    # @return [Array] a list with the resultat of the search
+    def list(**options)
+      result = []
+
+      search = options[:pattern].to_s.downcase
+      group  = options[:group].to_s.downcase
+
+      @data.each do |item|
+        next if item.empty?
+        next unless group.empty? || group.eql?(item.group.to_s.downcase)
+
+        host    = item.host.to_s.downcase
+        comment = item.comment.to_s.downcase
+
+        next unless host =~ /^.*#{search}.*$/ || comment =~ /^.*#{search}.*$/
+
+        result.push(item)
+      end
+
+      result
+    end
+
+    # Search an item with an id
+    # @param id [String]the id item
+    # @return [Item] an item or nil
+    def search_by_id(id)
+      @data.each do |item|
+        return item if item.id == id
+      end
+
+      nil
+    end
+
+    # Set a new opt key
+    # @param id [String] the item id
+    # @param key [String] the new key
+    def set_otp_key(id, key)
+      @otp_keys[id] = encrypt(key.to_s) unless key.to_s.empty?
+    end
+
+    # Get an opt key
+    # @param id [String] the item id
+    def get_otp_key(id)
+      @otp_keys.key?(id) ? decrypt(@otp_keys[id]) : nil
+    end
+
+    # Get an otp code
+    # @param id [String] the item id
+    # @return [String] an otp code
+    def get_otp_code(id)
+      @otp_keys.key?(id) ? ROTP::TOTP.new(decrypt(@otp_keys[id])).now : 0
+    end
+
+    # Get remaining time before expire otp code
+    # @return [Integer] time in seconde
+    def get_otp_remaining_time
+      (Time.now.utc.to_i / 30 + 1) * 30 - Time.now.utc.to_i
+    end
+
+    # Generate a random password
+    # @param options [Hash] :length, :special, :alpha, :numeric
+    # @return [String] a random string
+    def self.password(**options)
+      length =
+        if !options.include?(:length) || options[:length].to_i <= 0
+          8
+        elsif options[:length].to_i >= 32_768
+          32_768
+        else
+          options[:length].to_i
+        end
+
+      chars = []
+      chars += [*('!'..'?')] - [*('0'..'9')]          if options[:special]
+      chars += [*('A'..'Z'), *('a'..'z')]             if options[:alpha]
+      chars += [*('0'..'9')]                          if options[:numeric]
+      chars = [*('A'..'Z'), *('a'..'z'), *('0'..'9')] if chars.empty?
+
+      result = ''
+      length.times do
+        result << chars.sample
+      end
+
+      result
+    end
+
+    private
+
+    # Decrypt a gpg file
+    # @param data [String] data to decrypt
+    # @return [String] data decrypted
+    def decrypt(data)
+      return nil if data.to_s.empty?
+
+      password =
+        if /^(1\.[0-9.]+|2\.0)(\.[0-9]+)?/ =~ GPGME::Engine.info.first.version || @pinmode
+          { password: @gpg_pass }
+        else
+          { password: @gpg_pass,
+            pinentry_mode: GPGME::PINENTRY_MODE_LOOPBACK }
+        end
+
+      crypto = GPGME::Crypto.new(armor: true)
+      crypto
+        .decrypt(data, password)
+        .read.force_encoding('utf-8')
+    rescue => e
+      raise "#{I18n.t('error.gpg_file.decrypt')}\n#{e}"
+    end
+
+    # Encrypt a file
+    # @param data [String] data to encrypt
+    # @return [String] data encrypted
+    def encrypt(data)
+      recipients = []
+      crypto     = GPGME::Crypto.new(armor: true, always_trust: true)
+
+      recipients.push(@key)
+      @keys.each_key do |key|
+        next if key == @key
+        recipients.push(key)
+      end
+
+      crypto.encrypt(data, recipients: recipients).read
+    rescue => e
+      raise "#{I18n.t('error.gpg_file.encrypt')}\n#{e}"
+    end
+  end
+end
diff --git a/mpw b/mpw
deleted file mode 100755
index ff27575..0000000
--- a/mpw
+++ /dev/null
@@ -1,145 +0,0 @@
-#!/usr/bin/ruby
-# author: nishiki
-# mail: nishiki@yaegashi.fr
-# info: a simple script who manage your passwords
-
-require 'rubygems'
-require 'optparse'
-require 'pathname'
-require 'locale'
-require 'i18n'
-
-APP_ROOT = File.dirname(Pathname.new(__FILE__).realpath)
-require "#{APP_ROOT}/lib/Cli.rb"
-require "#{APP_ROOT}/lib/MPW.rb"
-
-lang = Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1]
-
-I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
-I18n.load_path = Dir["#{APP_ROOT}/i18n/cli/*.yml"]
-I18n.default_locale = :en
-I18n.locale = lang.to_sym
-
-options = {}
-options[:force]  = false
-options[:format] = false
-options[:group]  = nil
-options[:config] = nil
-
-OptionParser.new do |opts|
-	opts.banner = "#{I18n.t('option.usage')}: mpw [options]"
-
-	opts.on('-d', '--display [SEARCH]', I18n.t('option.show')) do |search|
-		search.nil? ? (options[:display]  = '')  : (options[:display] = search)
-	end
-
-	opts.on('-A', '--show-all', I18n.t('option.show_all')) do
-		options[:type]    = nil
-		options[:display] = ''
-	end
-
-	opts.on('-u', '--update ID', I18n.t('option.update')) do |id|
-		options[:update] = id
-	end
-
-	opts.on('-r', '--remove ID', I18n.t('option.remove')) do |id|
-		options[:remove] = id
-	end
-
-	opts.on('-g', '--group GROUP', I18n.t('option.group')) do |group|
-		options[:group] = group
-	end
-
-	opts.on('-a', '--add', I18n.t('option.add')) do
-		options[:add] = true
-	end
-
-	opts.on('-c', '--config CONFIG', I18n.t('option.config')) do |config|
-		options[:config] = config
-	end
-
-	opts.on('-S', '--setup', I18n.t('option.setup')) do
-		options[:setup] = true
-	end
-
-	opts.on('-p', '--protocol PROTOCOL', I18n.t('option.protocol')) do |type|
-		options[:type] = type
-	end
-
-	opts.on('-e', '--export FILE', I18n.t('option.export')) do |file|
-		options[:export] = file
-	end
-
-	opts.on('-i', '--import FILE', I18n.t('option.import')) do |file|
-		options[:import] = file
-	end
-
-	opts.on('-f', '--force', I18n.t('option.force')) do
-		options[:force] = true
-	end
-
-	opts.on('-F', '--format', I18n.t('option.format')) do
-		options[:format] = true
-	end
-
-	opts.on('-G', '--generate-password [LENGTH]', I18n.t('option.generate_password')) do |length|
-		puts MPW::password(length)
-		exit 0
-	end
-
-	opts.on('-h', '--help', I18n.t('option.help')) do
-		puts opts
-		exit 0
-	end
-end.parse!
-
-config      = MPWConfig.new(options[:config])
-check_error = config.checkconfig()
-
-cli = Cli.new(lang, config)
-	
-# Setup a new config 
-if !check_error || !options[:setup].nil?
-	cli.setup(lang)
-end
-
-cli.decrypt()
-cli.sync()
-
-# Display the item's informations
-if not options[:display].nil?
-	cli.display(options[:display], options[:group], options[:type], options[:format])
-
-# Remove an item
-elsif not options[:remove].nil?
-	cli.remove(options[:remove], options[:force])
-
-# Update an item
-elsif not options[:update].nil?
-	cli.update(options[:update])
-
-# Add a new item
-elsif not options[:add].nil?
-	cli.add()
-
-# Export
-elsif not options[:export].nil?
-	cli.export(options[:export])
-
-# Add a new item
-elsif not options[:import].nil?
-	cli.import(options[:import], options[:force])
-
-# Interactive mode
-else
-	begin
-		cli.interactive()
-	rescue SystemExit, Interrupt
-		cli = nil
-		return 1
-	end
-end
-
-cli.sync_close()
-
-exit 0
diff --git a/mpw-server b/mpw-server
deleted file mode 100755
index 07b42f5..0000000
--- a/mpw-server
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/ruby
-# author: nishiki
-# mail: nishiki@yaegashi.fr
-# info: a simple script who manage your passwords
-
-require 'rubygems'
-require 'optparse'
-require 'pathname'
-require 'locale'
-require 'i18n'
-
-APP_ROOT = File.dirname(Pathname.new(__FILE__).realpath)
-require "#{APP_ROOT}/lib/Server.rb"
-
-lang = Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1]
-
-I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
-I18n.load_path = Dir["#{APP_ROOT}/i18n/server/*.yml"]
-I18n.default_locale = :en
-I18n.locale = lang.to_sym
-
-options = {}
-OptionParser.new do |opts|
-	opts.banner = "#{I18n.t('option.usage')}: mpw-server -c CONFIG [options]"
-
-	opts.on("-c", "--config CONFIG", I18n.t('option.config')) do |config|
-		options[:config] = config
-	end
-
-	opts.on("-t", "--checkconfig", I18n.t('option.checkconfig')) do |b|
-		options[:checkconfig] = b
-	end
-
-	opts.on("-s", "--setup", I18n.t('option.setup')) do |b|
-		options[:setup] = b
-	end
-
-	opts.on("-h", "--help", I18n.t('option.help')) do |b|
-		puts opts
-		exit 0
-	end
-end.parse!
-
-if options[:config].nil? || options[:config].empty?
-	puts "#{I18n.t('option.usage')}: mpw-server -c CONFIG [options]"
-	exit 2
-end
-
-server = Server.new
-
-if options[:checkconfig]
-	server.checkconfig(options[:config])
-elsif options[:setup]
-	server.setup(options[:config])
-else
-	if server.checkconfig(options[:config])
-		server.start()
-	end
-end
-
-exit 0
diff --git a/mpw-ssh b/mpw-ssh
deleted file mode 100755
index 39fe1b3..0000000
--- a/mpw-ssh
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/ruby
-# author: nishiki
-# mail: nishiki@yaegashi.fr
-# info: a simple script who manage your passwords
-
-require 'rubygems'
-require 'optparse'
-require 'pathname'
-require 'locale'
-require 'i18n'
-
-APP_ROOT = File.dirname(Pathname.new(__FILE__).realpath)
-require "#{APP_ROOT}/lib/CliSSH.rb"
-
-lang = Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1]
-
-I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
-I18n.load_path = Dir["#{APP_ROOT}/i18n/cli/*.yml"]
-I18n.default_locale = :en
-I18n.locale = lang.to_sym
-
-options = {}
-OptionParser.new do |opts|
-	opts.banner = "#{I18n.t('ssh.option.usage')}: mpw-ssh SEARCH [options]"
-
-	opts.on("-l", "--login LOGIN", I18n.t('ssh.option.login')) do |login|
-		options[:login] = login
-	end
-
-	opts.on("-s", "--server SERVER", I18n.t('ssh.option.server')) do |server|
-		options[:server] = server
-	end
-
-	opts.on("-p", "--port PORT", I18n.t('ssh.option.port')) do |port|
-		options[:port] = port	
-	end
-
-	opts.on('-c', '--config CONFIG', I18n.t('cli.option.config')) do |config|
-		options[:config] = config
-	end
-
-	opts.on("-h", "--help", I18n.t('ssh.option.help')) do
-		puts opts
-		exit 0
-	end
-end.parse!
-
-config      = MPWConfig.new(options[:config])
-check_error = config.checkconfig()
-
-cli         = CliSSH.new(lang, config)
-cli.login   = options[:login]
-cli.server  = options[:server]
-cli.port    = options[:port]
-
-search     = ARGV[0]
-
-# Setup a new config 
-if !check_error 
-	cli.setup(lang)
-
-elsif ARGV.length < 1
-	puts "#{I18n.t('ssh.option.usage')}: mpw-ssh SEARCH [options]"
-	exit 2
-else
-	cli.decrypt()
-	cli.sync()
-	cli.ssh(search)
-end
-
-cli.sync_close()
-
-exit 0
diff --git a/mpw.gemspec b/mpw.gemspec
new file mode 100644
index 0000000..abcb375
--- /dev/null
+++ b/mpw.gemspec
@@ -0,0 +1,28 @@
+lib = File.expand_path('../lib', __FILE__)
+$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
+
+Gem::Specification.new do |spec|
+  spec.name          = 'mpw'
+  spec.version       = File.open('VERSION').read
+  spec.authors       = ['Adrien Waksberg']
+  spec.email         = ['mpw@yae.im']
+  spec.summary       = 'MPW is a software to crypt and manage your passwords'
+  spec.description   = 'Manage your passwords in all security with MPW, we use GPG to encrypt your passwords'
+  spec.homepage      = 'https://github.com/nishiki/manage-password'
+  spec.license       = 'GPL-2.0'
+
+  spec.files         = %x(git ls-files -z).split("\x0")
+  spec.executables   = ['mpw']
+  spec.test_files    = spec.files.grep(%r{^(test|spec|features)/})
+  spec.require_paths = ['lib']
+
+  spec.required_ruby_version = '>= 2.1'
+
+  spec.add_dependency 'i18n',      '~> 0.9', '>= 0.9.1'
+  spec.add_dependency 'gpgme',     '~> 2.0', '>= 2.0.14'
+  spec.add_dependency 'highline',  '~> 1.7', '>= 1.7.8'
+  spec.add_dependency 'locale',    '~> 2.1', '>= 2.1.2'
+  spec.add_dependency 'colorize',  '~> 0.8', '>= 0.8.1'
+  spec.add_dependency 'clipboard', '~> 1.1', '>= 1.1.1'
+  spec.add_dependency 'rotp',      '~> 3.3', '>= 3.3.0'
+end
diff --git a/templates/add_form.erb b/templates/add_form.erb
new file mode 100644
index 0000000..e2aea20
--- /dev/null
+++ b/templates/add_form.erb
@@ -0,0 +1,13 @@
+---
+# <%= I18n.t('form.add_item.url') %>
+url: <%= options[:url] %>
+# <%= I18n.t('form.add_item.login') %>
+user: <%= options[:user] %>
+# <%= I18n.t('form.add_item.group') %>
+group: <%= options[:group] %><% unless password %>
+# <%= I18n.t('form.add_item.password') %>
+password:<% end %>
+# <%= I18n.t('form.add_item.comment') %>
+comment: <%= options[:comment] %>
+# <%= I18n.t('form.add_item.otp_key') %>
+otp_key: <%= options[:otp] %>
diff --git a/templates/update_form.erb b/templates/update_form.erb
new file mode 100644
index 0000000..eafeb1b
--- /dev/null
+++ b/templates/update_form.erb
@@ -0,0 +1,13 @@
+---
+# <%= I18n.t('form.update_item.url') %>
+host: <%  if options[:url] %><%= options[:url] %><% else %><%= item.url %><% end %>
+# <%= I18n.t('form.update_item.login') %>
+user: <%  if options[:user] %><%= options[:user] %><% else %><%= item.user %><% end %><% unless password %>
+# <%= I18n.t('form.update_item.password') %>
+password: <% end %>
+# <%= I18n.t('form.update_item.group') %>
+group: <%  if options[:group] %><%= options[:group] %><% else %><%= item.group %><% end %>
+# <%= I18n.t('form.update_item.otp_key') %>
+otp_key: <%  if options[:otp_key] %><%= options[:otp_key] %><% end %>
+# <%= I18n.t('form.update_item.comment') %>
+comment: <%  if options[:comment] %><%= options[:comment] %><% else %><%= item.comment %><% end %>
diff --git a/test/files/fixtures-import.yml b/test/files/fixtures-import.yml
new file mode 100644
index 0000000..8cb1ff4
--- /dev/null
+++ b/test/files/fixtures-import.yml
@@ -0,0 +1,16 @@
+---
+1:
+  url: https://fric.com
+  user: 230403
+  group: Bank
+  password: 5XdiTQOubRDw9B0aJoMlcEyL
+  otp_key: 330223432
+  comment: I love my bank
+2:
+  url: https://assurance.com:443
+  user: user_2132
+  host: assurance.com
+  group: Assurance
+  password: DMyK6B3v4bWO52VzU7aTHIem
+  otp_key: 
+  comment: 
diff --git a/test/files/fixtures.yml b/test/files/fixtures.yml
new file mode 100644
index 0000000..d8927cb
--- /dev/null
+++ b/test/files/fixtures.yml
@@ -0,0 +1,31 @@
+add:
+  url: 'https://example.com:8080'
+  group: 'Bank'
+  host: 'example.com'
+  protocol: 'https'
+  user: 'admin'
+  password: 'VmfnCN6pPIqgRIbc'
+  port: '8080'
+  comment: 'the website'
+
+import:
+  id: 'TEST-ID-XXXXX'
+  url: 'https://gogole.com:8081/toto'
+  group: 'Cloud'
+  host: 'gogole.com'
+  protocol: 'https'
+  user: 'gg-2304'
+  password: 'TITl0kV9CDDa9sVK'
+  port: '8081'
+  comment: 'My little servers'
+  created: 1386752948
+
+update:
+  url: 'ssh://example2.com:2222'
+  group: 'Assurance'
+  host: 'example2.com'
+  protocol: 'ssh'
+  user: 'root'
+  password: 'kbSrbv4WlMaVxaZ7'
+  port: '2222'
+  comment: 'i love ssh'
diff --git a/test/files/import-gorilla.txt b/test/files/import-gorilla.txt
new file mode 100644
index 0000000..a5fc604
--- /dev/null
+++ b/test/files/import-gorilla.txt
@@ -0,0 +1,4 @@
+uuid,group,title,url,user,password,notes
+49627979-e393-48c4-49ca-1cf66603238e,Bank,Fric,http://fric.com,12345,secret,money money
+49627979-e393-48c4-49ca-1cf66603238f,,My little server,server.com,secret2,
+49627979-e393-48c4-49ca-1cf66603238g,Cloud,,ssh://fric.com:4333,username,secret,bastion
diff --git a/test/files/import-keepass.txt b/test/files/import-keepass.txt
new file mode 100644
index 0000000..61ce7ad
--- /dev/null
+++ b/test/files/import-keepass.txt
@@ -0,0 +1,3 @@
+"Group","Title","Username","Password","URL","Notes"
+"Racine","Bank","123456","ywcExJW8qmBVTSyi","http://bank.com/login","My little bank"
+"Racine/Cloud","GAFAM","wesh","superpassword","localhost.local",""
diff --git a/test/files/import-mpw_old.txt b/test/files/import-mpw_old.txt
new file mode 100644
index 0000000..fd162aa
--- /dev/null
+++ b/test/files/import-mpw_old.txt
@@ -0,0 +1,35 @@
+---
+1:
+  host: fric.com
+  user: 12345
+  group: Bank
+  password: secret
+  protocol: http
+  port:
+  otp_key: 
+  comment: Fric money money 
+  last_edit: 1487623641
+  created: 1485729356
+2:
+  host: server.com
+  user: sercret2
+  group:
+  password:
+  protocol:
+  port: 4222
+  otp_key: 
+  comment: My little server
+  last_edit: 1487623641
+  created: 1485729356
+3:
+  host: fric.com
+  user: username
+  group: Cloud
+  password:
+  protocol: ssh
+  port: 4333
+  otp_key: 
+  comment: bastion
+  last_edit: 1487623641
+  created: 1485729356
+
diff --git a/test/init.rb b/test/init.rb
new file mode 100644
index 0000000..08ebbb8
--- /dev/null
+++ b/test/init.rb
@@ -0,0 +1,21 @@
+require 'fileutils'
+require 'gpgme'
+
+FileUtils.rm_rf("#{Dir.home}/.config/mpw")
+FileUtils.rm_rf("#{Dir.home}/.gnupg")
+
+param = ''
+param << '<GnupgKeyParms format="internal">' + "\n"
+param << "Key-Type: RSA\n"
+param << "Key-Length: 512\n"
+param << "Subkey-Type: ELG-E\n"
+param << "Subkey-Length: 512\n"
+param << "Name-Real: test\n"
+param << "Name-Comment: test\n"
+param << "Name-Email: test2@example.com\n"
+param << "Expire-Date: 0\n"
+param << "Passphrase: password\n"
+param << "</GnupgKeyParms>\n"
+
+ctx = GPGME::Ctx.new
+ctx.genkey(param, nil, nil)
diff --git a/test/test_cli.rb b/test/test_cli.rb
new file mode 100644
index 0000000..8bae4cc
--- /dev/null
+++ b/test/test_cli.rb
@@ -0,0 +1,256 @@
+require 'i18n'
+require 'test/unit'
+
+class TestConfig < Test::Unit::TestCase
+  def setup
+    if defined?(I18n.enforce_available_locales)
+      I18n.enforce_available_locales = true
+    end
+
+    I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
+    I18n.load_path = ["#{File.expand_path('../../i18n', __FILE__)}/en.yml"]
+    I18n.locale    = :en
+
+    @password = 'password'
+    @fixtures = YAML.load_file('./test/files/fixtures.yml')
+    @gpg_key  = 'test@example.com'
+  end
+
+  def test_00_init_config
+    output = %x(
+      echo "#{@password}\n#{@password}" | mpw config \
+      --init #{@gpg_key} \
+      2>/dev/null
+    )
+    assert_match(I18n.t('form.setup_config.valid'), output)
+    assert_match(I18n.t('form.setup_gpg_key.valid'), output)
+  end
+
+  def test_01_add_item
+    data = @fixtures['add']
+
+    output = %x(
+      echo #{@password} | mpw add \
+      --url #{data['url']} \
+      --user #{data['user']} \
+      --comment '#{data['comment']}' \
+      --group #{data['group']} \
+      --random \
+      2>/dev/null
+    )
+    assert_match(I18n.t('form.add_item.valid'), output)
+
+    output = %x(echo #{@password} | mpw list 2>/dev/null)
+    assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output)
+    assert_match(data['user'], output)
+    assert_match(data['comment'], output)
+    assert_match(data['group'], output)
+  end
+
+  def test_02_search
+    data = @fixtures['add']
+
+    output = %x(echo #{@password} | mpw list --group #{data['group']} 2>/dev/null)
+    assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output)
+
+    output = %x(echo #{@password} | mpw list --pattern #{data['host']} 2>/dev/null)
+    assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output)
+
+    output = %x(echo #{@password} | mpw list --pattern #{data['comment']} 2>/dev/null)
+    assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output)
+
+    output = %x(echo #{@password} | mpw list --group R1Pmfbp626TFpjlr 2>/dev/null)
+    assert_match(I18n.t('display.nothing'), output)
+
+    output = %x(echo #{@password} | mpw list --pattern h1IfnKqamaGM9oEX 2>/dev/null)
+    assert_match(I18n.t('display.nothing'), output)
+  end
+
+  def test_03_update_item
+    data = @fixtures['update']
+
+    output = %x(
+      echo #{@password} | mpw update \
+      -p #{@fixtures['add']['host']} \
+      --url #{data['url']} \
+      --user #{data['user']} \
+      --comment '#{data['comment']}' \
+      --new-group #{data['group']} \
+      2>/dev/null
+    )
+    assert_match(I18n.t('form.update_item.valid'), output)
+
+    output = %x(echo #{@password} | mpw list 2>/dev/null)
+    assert_match(%r{#{data['protocol']}://.+#{data['host']}.+:#{data['port']}}, output)
+    assert_match(data['user'], output)
+    assert_match(data['comment'], output)
+    assert_match(data['group'], output)
+  end
+
+  def test_04_delete_item
+    output = %x(
+      echo "#{@password}\ny" | mpw delete \
+      -p #{@fixtures['update']['host']} \
+      2>/dev/null
+    )
+    assert_match(I18n.t('form.delete_item.valid'), output)
+
+    output = %x(echo #{@password} | mpw list 2>/dev/null)
+    assert_match(I18n.t('display.nothing'), output)
+  end
+
+  def test_05_import_export
+    file_import = './test/files/fixtures-import.yml'
+    file_export = '/tmp/test-mpw.yml'
+
+    output = %x(echo #{@password} | mpw import --file #{file_import} 2>/dev/null)
+    assert_match(I18n.t('form.import.valid', file: file_import), output)
+
+    output = %x(echo #{@password} | mpw export --file #{file_export} 2>/dev/null)
+    assert_match(I18n.t('form.export.valid', file: file_export), output)
+    assert(File.exist?(file_export))
+    assert_equal(YAML.load_file(file_export).length, 2)
+
+    YAML.load_file(file_import).each_value do |import|
+      error = true
+
+      YAML.load_file(file_export).each_value do |export|
+        next if import['url'] != export['url']
+
+        %w[user group password protocol port otp_key comment].each do |key|
+          assert_equal(import[key].to_s, export[key].to_s)
+        end
+
+        error = false
+        break
+      end
+
+      assert(!error)
+    end
+  end
+
+  def test_06_copy
+    data = YAML.load_file('./test/files/fixtures-import.yml')[2]
+
+    output = %x(
+      echo "#{@password}\np\nq" | mpw copy \
+      --disable-clipboard \
+      -p #{data['host']} \
+      2>/dev/null
+    )
+    assert_match(data['password'], output)
+  end
+
+  def test_07_setup_wallet
+    gpg_key = 'test2@example.com'
+
+    output = %x(echo #{@password} | mpw wallet --add-gpg-key #{gpg_key} 2>/dev/null)
+    assert_match(I18n.t('form.add_key.valid'), output)
+
+    output = %x(echo #{@password} | mpw wallet --list-keys 2>/dev/null)
+    assert_match("| #{@gpg_key}", output)
+    assert_match("| #{gpg_key}", output)
+
+    output = %x(echo #{@password} | mpw wallet --delete-gpg-key #{gpg_key} 2>/dev/null)
+    assert_match(I18n.t('form.delete_key.valid'), output)
+
+    output = %x(echo #{@password} | mpw wallet --list-keys 2>/dev/null)
+    assert_match("| #{@gpg_key}", output)
+    assert_no_match(/\| #{gpg_key}/, output)
+
+    output = %x(mpw wallet)
+    assert_match('| default', output)
+
+    output = %x(mpw wallet --path '.')
+    assert_match(I18n.t('form.set_wallet_path.valid'), output)
+
+    output = %x(mpw config)
+    assert_match(%r{path_wallet_default.+\| #{Dir.pwd}/default.mpw}, output)
+    assert(File.exist?("#{Dir.pwd}/default.mpw"))
+
+    output = %x(mpw wallet)
+    assert_match('default', output)
+
+    output = %x(mpw wallet --default-path)
+    assert_match(I18n.t('form.set_wallet_path.valid'), output)
+
+    output = %x(mpw config)
+    assert_no_match(/path_wallet_default/, output)
+  end
+
+  def test_08_setup_config
+    gpg_key    = 'test2@example.com'
+    gpg_exe    = '/usr/bin/gpg2'
+    wallet_dir = '/tmp'
+    length     = 24
+    wallet     = 'work'
+
+    output = %x(
+      mpw config \
+      --gpg-exe #{gpg_exe} \
+      --key #{gpg_key} \
+      --enable-pinmode \
+      --disable-alpha \
+      --disable-special-chars \
+      --disable-numeric \
+      --length #{length} \
+      --wallet-dir #{wallet_dir} \
+      --default-wallet #{wallet}
+    )
+    assert_match(I18n.t('form.set_config.valid'), output)
+
+    output = %x(mpw config)
+    assert_match(/gpg_key.+\| #{gpg_key}/, output)
+    assert_match(/gpg_exe.+\| #{gpg_exe}/, output)
+    assert_match(/pinmode.+\| true/, output)
+    assert_match(/default_wallet.+\| #{wallet}/, output)
+    assert_match(/wallet_dir.+\| #{wallet_dir}/, output)
+    assert_match(/password_length.+\| #{length}/, output)
+    %w[numeric alpha special].each do |k|
+      assert_match(/password_#{k}.+\| false/, output)
+    end
+
+    output = %x(
+      mpw config \
+      --gpg-exe '' \
+      --key #{@gpg_key} \
+      --alpha \
+      --special-chars \
+      --numeric \
+      --disable-pinmode
+    )
+    assert_match(I18n.t('form.set_config.valid'), output)
+
+    output = %x(mpw config)
+    assert_match(/gpg_key.+\| #{@gpg_key}/, output)
+    assert_match(/pinmode.+\| false/, output)
+    %w[numeric alpha special].each do |k|
+      assert_match(/password_#{k}.+\| true/, output)
+    end
+  end
+
+  def test_09_generate_password
+    length = 24
+
+    output = %x(
+      mpw genpwd \
+      --length #{length} \
+      --alpha
+    )
+    assert_match(/[a-zA-Z]{#{length}}/, output)
+
+    output = %x(
+      mpw genpwd \
+      --length #{length} \
+      --numeric
+    )
+    assert_match(/[0-9]{#{length}}/, output)
+
+    output = %x(
+      mpw genpwd \
+      --length #{length} \
+      --special-chars
+    )
+    assert_no_match(/[a-zA-Z0-9]/, output)
+  end
+end
diff --git a/test/test_config.rb b/test/test_config.rb
new file mode 100644
index 0000000..2b88b54
--- /dev/null
+++ b/test/test_config.rb
@@ -0,0 +1,79 @@
+require 'mpw/config'
+require 'test/unit'
+require 'locale'
+require 'i18n'
+
+class TestConfig < Test::Unit::TestCase
+  def setup
+    lang = Locale::Tag.parse(ENV['LANG']).to_simple.to_s[0..1]
+
+    if defined?(I18n.enforce_available_locales)
+      I18n.enforce_available_locales = true
+    end
+
+    I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
+    I18n.load_path      = Dir["#{File.expand_path('../../i18n', __FILE__)}/*.yml"]
+    I18n.default_locale = :en
+    I18n.locale         = lang.to_sym
+  end
+
+  def test_00_config
+    data = {
+      gpg_key: 'test@example.com',
+      lang: 'en',
+      wallet_dir: '/tmp/test',
+      gpg_exe: ''
+    }
+
+    @config = MPW::Config.new
+    @config.setup(data)
+    @config.load_config
+
+    data.each do |k, v|
+      assert_equal(v, @config.send(k))
+    end
+
+    @config.setup_gpg_key('password', 'test@example.com', 2048)
+    assert(@config.check_gpg_key?)
+  end
+
+  def test_01_password
+    data = {
+      pwd_alpha: false,
+      pwd_numeric: false,
+      pwd_special: true,
+      pwd_length: 32
+    }
+
+    @config = MPW::Config.new
+    @config.load_config
+
+    assert_equal(@config.password[:length], 16)
+    assert(@config.password[:alpha])
+    assert(@config.password[:numeric])
+    assert(!@config.password[:special])
+
+    @config.setup(data)
+    @config.load_config
+
+    assert_equal(@config.password[:length], data[:pwd_length])
+    assert(!@config.password[:alpha])
+    assert(!@config.password[:numeric])
+    assert(@config.password[:special])
+  end
+
+  def test_02_wallet_paths
+    new_path = '/tmp/mpw-test'
+
+    @config = MPW::Config.new
+    @config.load_config
+
+    assert(!@config.wallet_paths['default'])
+
+    @config.set_wallet_path(new_path, 'default')
+    assert_equal(@config.wallet_paths['default'], new_path)
+
+    @config.set_wallet_path('default', 'default')
+    assert(!@config.wallet_paths['default'])
+  end
+end
diff --git a/test/test_import.rb b/test/test_import.rb
new file mode 100644
index 0000000..f1a8727
--- /dev/null
+++ b/test/test_import.rb
@@ -0,0 +1,79 @@
+require 'i18n'
+require 'test/unit'
+
+class TestImport < Test::Unit::TestCase
+  def setup
+    if defined?(I18n.enforce_available_locales)
+      I18n.enforce_available_locales = true
+    end
+
+    I18n::Backend::Simple.send(:include, I18n::Backend::Fallbacks)
+    I18n.load_path = ["#{File.expand_path('../../i18n', __FILE__)}/en.yml"]
+    I18n.locale    = :en
+
+    @password = 'password'
+  end
+
+  def test_00_import_mpw_old
+    file = './test/files/import-mpw_old.txt'
+    format = 'mpw_old'
+
+    output = %x(
+        mpw import \
+        --file #{file} \
+        --format #{format} \
+        --wallet #{format}
+      )
+    assert_match(I18n.t('form.import.valid'), output)
+
+    output = %x(echo #{@password} | mpw list --group Bank --wallet #{format})
+    assert_match(%r{http://.*fric\.com.*12345.*Fric money money}, output)
+
+    output = %x(echo #{@password} | mpw list --group Cloud --wallet #{format})
+    assert_match(%r{ssh://.*fric\.com.*:4333.*username.*bastion}, output)
+
+    output = %x(echo #{@password} | mpw list --wallet #{format})
+    assert_match(/server\.com.*My little server/, output)
+  end
+
+  def test_01_import_gorilla
+    file = './test/files/import-gorilla.txt'
+    format = 'gorilla'
+
+    output = %x(
+        mpw import \
+        --file #{file} \
+        --format #{format} \
+        --wallet #{format}
+      )
+    assert_match(I18n.t('form.import.valid'), output)
+
+    output = %x(echo #{@password} | mpw list --group Bank --wallet #{format})
+    assert_match(%r{http://.*fric\.com.*12345.*Fric money money}, output)
+
+    output = %x(echo #{@password} | mpw list --group Cloud --wallet #{format})
+    assert_match(%r{ssh://.*fric\.com.*:4333.*username.*bastion}, output)
+
+    output = %x(echo #{@password} | mpw list --wallet #{format})
+    assert_match(/server\.com.*My little server/, output)
+  end
+
+  def test_02_import_keepass
+    file = './test/files/import-keepass.txt'
+    format = 'keepass'
+
+    output = %x(
+        mpw import \
+        --file #{file} \
+        --format #{format} \
+        --wallet #{format}
+      )
+    assert_match(I18n.t('form.import.valid'), output)
+
+    output = %x(echo #{@password} | mpw list --group 'Racine/Cloud' --wallet #{format})
+    assert_match(/localhost\.local.*wesh.*GAFAM/, output)
+
+    output = %x(echo #{@password} | mpw list --wallet #{format})
+    assert_match(%r{http://.*bank\.com.*123456.*Bank My little bank}, output)
+  end
+end
diff --git a/test/test_item.rb b/test/test_item.rb
new file mode 100644
index 0000000..97503c0
--- /dev/null
+++ b/test/test_item.rb
@@ -0,0 +1,165 @@
+require 'mpw/item'
+require 'test/unit'
+require 'yaml'
+
+class TestItem < Test::Unit::TestCase
+  def setup
+    if defined?(I18n.enforce_available_locales)
+      I18n.enforce_available_locales = false
+    end
+
+    I18n.load_path      = Dir['./i18n/cli/*.yml']
+    I18n.default_locale = :en
+
+    @fixtures = YAML.load_file('./test/files/fixtures.yml')
+  end
+
+  def test_00_add_without_name
+    assert_raise(RuntimeError) { MPW::Item.new }
+  end
+
+  def test_01_add
+    data = {
+      group:    @fixtures['add']['group'],
+      user:     @fixtures['add']['user'],
+      url:      @fixtures['add']['url'],
+      comment:  @fixtures['add']['comment']
+    }
+
+    item = MPW::Item.new(data)
+
+    assert(!item.nil?)
+    assert(!item.empty?)
+
+    assert_equal(@fixtures['add']['url'],       item.url)
+    assert_equal(@fixtures['add']['group'],     item.group)
+    assert_equal(@fixtures['add']['host'],      item.host)
+    assert_equal(@fixtures['add']['protocol'],  item.protocol)
+    assert_equal(@fixtures['add']['user'],      item.user)
+    assert_equal(@fixtures['add']['port'].to_i, item.port)
+    assert_equal(@fixtures['add']['comment'],   item.comment)
+  end
+
+  def test_02_import
+    data = {
+      id:       @fixtures['import']['id'],
+      group:    @fixtures['import']['group'],
+      user:     @fixtures['import']['user'],
+      url:      @fixtures['import']['url'],
+      comment:  @fixtures['import']['comment'],
+      created:  @fixtures['import']['created']
+    }
+
+    item = MPW::Item.new(data)
+
+    assert(!item.nil?)
+    assert(!item.empty?)
+
+    assert_equal(@fixtures['import']['id'],        item.id)
+    assert_equal(@fixtures['import']['url'],       item.url)
+    assert_equal(@fixtures['import']['group'],     item.group)
+    assert_equal(@fixtures['import']['host'],      item.host)
+    assert_equal(@fixtures['import']['protocol'],  item.protocol)
+    assert_equal(@fixtures['import']['user'],      item.user)
+    assert_equal(@fixtures['import']['port'].to_i, item.port)
+    assert_equal(@fixtures['import']['comment'],   item.comment)
+    assert_equal(@fixtures['import']['created'],   item.created)
+  end
+
+  def test_03_update
+    data = {
+      group:    @fixtures['add']['group'],
+      user:     @fixtures['add']['user'],
+      url:      @fixtures['add']['url'],
+      comment:  @fixtures['add']['comment']
+    }
+
+    item = MPW::Item.new(data)
+
+    assert(!item.nil?)
+    assert(!item.empty?)
+
+    created   = item.created
+    last_edit = item.last_edit
+
+    data = {
+      group:    @fixtures['update']['group'],
+      user:     @fixtures['update']['user'],
+      url:      @fixtures['update']['url'],
+      comment:  @fixtures['update']['comment']
+    }
+
+    sleep(1)
+    assert(item.update(data))
+
+    assert(!item.empty?)
+
+    assert_equal(@fixtures['update']['url'],       item.url)
+    assert_equal(@fixtures['update']['group'],     item.group)
+    assert_equal(@fixtures['update']['host'],      item.host)
+    assert_equal(@fixtures['update']['protocol'],  item.protocol)
+    assert_equal(@fixtures['update']['user'],      item.user)
+    assert_equal(@fixtures['update']['port'].to_i, item.port)
+    assert_equal(@fixtures['update']['comment'],   item.comment)
+
+    assert_equal(created, item.created)
+    assert_not_equal(last_edit, item.last_edit)
+  end
+
+  def test_05_update_one_element
+    data = {
+      group:    @fixtures['add']['group'],
+      user:     @fixtures['add']['user'],
+      url:      @fixtures['add']['url'],
+      comment:  @fixtures['add']['comment']
+    }
+
+    item = MPW::Item.new(data)
+
+    assert(!item.nil?)
+    assert(!item.empty?)
+
+    last_edit = item.last_edit
+
+    sleep(1)
+    item.update(comment: @fixtures['update']['comment'])
+
+    assert_equal(@fixtures['add']['url'],        item.url)
+    assert_equal(@fixtures['add']['group'],      item.group)
+    assert_equal(@fixtures['add']['host'],       item.host)
+    assert_equal(@fixtures['add']['protocol'],   item.protocol)
+    assert_equal(@fixtures['add']['user'],       item.user)
+    assert_equal(@fixtures['add']['port'].to_i,  item.port)
+    assert_equal(@fixtures['update']['comment'], item.comment)
+
+    assert_not_equal(last_edit, item.last_edit)
+  end
+
+  def test_05_delete
+    data = {
+      group:    @fixtures['add']['group'],
+      user:     @fixtures['add']['user'],
+      url:      @fixtures['add']['url'],
+      comment:  @fixtures['add']['comment']
+    }
+
+    item = MPW::Item.new(data)
+
+    assert(!item.nil?)
+    assert(!item.empty?)
+
+    item.delete
+    assert(!item.nil?)
+    assert(item.empty?)
+
+    assert_equal(nil, item.id)
+    assert_equal(nil, item.url)
+    assert_equal(nil, item.group)
+    assert_equal(nil, item.host)
+    assert_equal(nil, item.protocol)
+    assert_equal(nil, item.user)
+    assert_equal(nil, item.port)
+    assert_equal(nil, item.comment)
+    assert_equal(nil, item.created)
+  end
+end
diff --git a/test/test_mpw.rb b/test/test_mpw.rb
new file mode 100644
index 0000000..8fc544e
--- /dev/null
+++ b/test/test_mpw.rb
@@ -0,0 +1,130 @@
+require 'mpw/mpw'
+require 'mpw/item'
+require 'test/unit'
+require 'yaml'
+require 'csv'
+
+class TestMPW < Test::Unit::TestCase
+  def setup
+    wallet_file = 'default.gpg'
+    key         = 'test@example.com'
+    password    = 'password'
+
+    if defined?(I18n.enforce_available_locales)
+      I18n.enforce_available_locales = false
+    end
+
+    @mpw      = MPW::MPW.new(key, wallet_file, password)
+    @fixtures = YAML.load_file('./test/files/fixtures.yml')
+  end
+
+  def test_00_decrypt_empty_file
+    @mpw.read_data
+    assert_equal(0, @mpw.list.length)
+  end
+
+  def test_01_encrypt_empty_file
+    @mpw.read_data
+    @mpw.write_data
+  end
+
+  def test_02_add_item
+    data = {
+      group:    @fixtures['add']['group'],
+      user:     @fixtures['add']['user'],
+      url:      @fixtures['add']['url'],
+      comment:  @fixtures['add']['comment']
+    }
+
+    item = MPW::Item.new(data)
+
+    assert(!item.nil?)
+    assert(!item.empty?)
+
+    @mpw.read_data
+    @mpw.add(item)
+    @mpw.set_password(item.id, @fixtures['add']['password'])
+
+    assert_equal(1, @mpw.list.length)
+
+    item = @mpw.list[0]
+    @fixtures['add'].each do |k, v|
+      if k == 'password'
+        assert_equal(v, @mpw.get_password(item.id))
+      else
+        assert_equal(v, item.send(k).to_s)
+      end
+    end
+
+    @mpw.write_data
+  end
+
+  def test_03_decrypt_file
+    @mpw.read_data
+    assert_equal(1, @mpw.list.length)
+
+    item = @mpw.list[0]
+    @fixtures['add'].each do |k, v|
+      if k == 'password'
+        assert_equal(v, @mpw.get_password(item.id))
+      else
+        assert_equal(v, item.send(k).to_s)
+      end
+    end
+  end
+
+  def test_04_delete_item
+    @mpw.read_data
+    assert_equal(1, @mpw.list.length)
+
+    @mpw.list.each(&:delete)
+    assert_equal(0, @mpw.list.length)
+
+    @mpw.write_data
+  end
+
+  def test_05_search
+    @mpw.read_data
+
+    @fixtures.each_value do |v|
+      data = {
+        group:    v['group'],
+        user:     v['user'],
+        url:      v['url'],
+        comment:  v['comment']
+      }
+
+      item = MPW::Item.new(data)
+
+      assert(!item.nil?)
+      assert(!item.empty?)
+
+      @mpw.add(item)
+      @mpw.set_password(item.id, v['password'])
+    end
+
+    assert_equal(3, @mpw.list.length)
+    assert_equal(1, @mpw.list(group:   @fixtures['add']['group']).length)
+    assert_equal(1, @mpw.list(pattern: 'gogole').length)
+    assert_equal(2, @mpw.list(pattern: 'example[2\.]').length)
+  end
+
+  def test_06_add_gpg_key
+    @mpw.read_data
+
+    @mpw.add_key('test2@example.com')
+    assert_equal(2, @mpw.list_keys.length)
+
+    @mpw.write_data
+  end
+
+  def test_07_delete_gpg_key
+    @mpw.read_data
+    assert_equal(2, @mpw.list_keys.length)
+
+    @mpw.delete_key('test2@example.com')
+    assert_equal(1, @mpw.list_keys.length)
+
+    @mpw.write_data
+  end
+end
diff --git a/test/test_translate.rb b/test/test_translate.rb
new file mode 100644
index 0000000..d7cbceb
--- /dev/null
+++ b/test/test_translate.rb
@@ -0,0 +1,29 @@
+require 'yaml'
+require 'test/unit'
+
+class TestTranslate < Test::Unit::TestCase
+  def test_00_check_translate
+    missing = 0
+
+    Dir.glob('i18n/*.yml').each do |yaml|
+      lang      = File.basename(yaml, '.yml')
+      translate = YAML.load_file(yaml)
+
+      %x(grep -r -o "I18n.t('.*)" bin/ lib/ | cut -d"'" -f2).each_line do |line|
+        begin
+          t = translate[lang]
+          line.strip.split('.').each do |v|
+            t = t[v]
+          end
+
+          assert(!t.to_s.empty?)
+        rescue
+          puts "#{lang}.#{line}"
+          missing = 1
+        end
+      end
+    end
+
+    assert_equal(0, missing)
+  end
+end