├── .github ├── FUNDING.yml └── workflows │ ├── master.yml │ └── release.yml ├── .gitignore ├── .hlint.yaml ├── .stylish-haskell.yaml ├── LICENSE ├── Makefile ├── README.md ├── RELEASE.md ├── Setup.hs ├── Theming.md ├── docs ├── Makefile ├── RELEASE.md ├── example_config.hs ├── out │ ├── default_theme.hs │ ├── kpxhs.1 │ └── kpxhs.1.md └── src │ ├── part-1.md │ ├── part-2.md │ └── section.py ├── kpxhs.cabal ├── pics ├── browser1.png ├── browser2.png ├── clear_clip.png ├── countdown.png ├── entry.png ├── responsive_footer.png ├── searching.png └── unlock.png ├── src └── kpxhs │ ├── Common.hs │ ├── Config │ ├── Config.hs │ ├── Defaults.hs │ ├── Eval.hs │ └── Types.hs │ ├── Constants.hs │ ├── Events.hs │ ├── Main.hs │ ├── Types.hs │ ├── UI.hs │ ├── UI │ ├── BrowserUI.hs │ ├── Common.hs │ ├── EntryDetailsUI.hs │ ├── ExitDialogUI.hs │ └── LoginUI.hs │ └── ViewEvents │ ├── BrowserEvents │ ├── BrowserEvents.hs │ ├── Core.hs │ ├── Event.hs │ ├── Fork.hs │ ├── Utils.hs │ └── VimCommand.hs │ ├── Common.hs │ ├── Copy.hs │ ├── EntryDetailsEvents.hs │ ├── ExitDialogEvents.hs │ ├── LoginEvents.hs │ ├── LoginFrozenEvents.hs │ ├── SearchEvents.hs │ └── Utils.hs ├── stack.yaml ├── stack.yaml.lock └── test ├── README.md ├── keyfile.key ├── kpxhs_test.kdbx └── kpxhs_test_no_keyfile.kdbx /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: paypal.me/tsuiyc 2 | -------------------------------------------------------------------------------- /.github/workflows/master.yml: -------------------------------------------------------------------------------- 1 | name: master 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | paths-ignore: 7 | - README.md 8 | - test/* 9 | - pics/* 10 | - LICENSE 11 | - Theming.md 12 | 13 | jobs: 14 | build: 15 | 16 | runs-on: ${{ matrix.os }} 17 | strategy: 18 | fail-fast: false 19 | matrix: 20 | include: 21 | - os: ubuntu-latest 22 | asset_name: kpxhs-linux 23 | - os: macos-latest 24 | asset_name: kpxhs-macos 25 | 26 | steps: 27 | - name: Checkout 28 | uses: actions/checkout@v2 29 | - name: Run hlint 30 | continue-on-error: true 31 | run: | 32 | curl -sSL https://raw.github.com/ndmitchell/hlint/master/misc/run.sh | sh -s src/kpxhs 33 | - name: Build 34 | run: make build 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build: 10 | 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | - os: ubuntu-latest 17 | asset_name: kpxhs-linux 18 | - os: macos-latest 19 | asset_name: kpxhs-macos 20 | 21 | steps: 22 | - name: Checkout 23 | uses: actions/checkout@v2 24 | - name: Install pandoc 25 | run: | 26 | if [ "$RUNNER_OS" == "Linux" ]; then 27 | sudo apt-get update 28 | sudo apt-get install pandoc 29 | else 30 | brew update 31 | brew install pandoc 32 | fi 33 | - name: Build docs 34 | run: | 35 | cd docs 36 | make 37 | - name: Build binary 38 | run: make install 39 | - name: Copy binary and rename 40 | run: | 41 | mkdir dist 42 | cp ~/.local/bin/kpxhs dist 43 | mv dist/kpxhs dist/${{ matrix.asset_name }} 44 | - name: Generate digest 45 | run: | 46 | name=${{ matrix.asset_name }} 47 | cd dist 48 | if [ "$RUNNER_OS" == "Linux" ]; then 49 | sha256sum "$name" > "${name}.DIGEST" 50 | else 51 | shasum -a 256 "$name" > "${name}.DIGEST" 52 | fi 53 | - uses: ncipollo/release-action@v1 54 | with: 55 | artifacts: 56 | dist/${{ matrix.asset_name }}, 57 | dist/${{ matrix.asset_name }}.DIGEST, 58 | docs/out/kpxhs.1 59 | token: ${{ secrets.GITHUB_TOKEN }} 60 | allowUpdates: true 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .stack-work/ 2 | -------------------------------------------------------------------------------- /.hlint.yaml: -------------------------------------------------------------------------------- 1 | - ignore: { name: "Use unless" } 2 | 3 | - functions: 4 | - {name: unsafeDupablePerformIO, within: []} # Unsafe 5 | - {name: unsafeInterleaveIO, within: []} # Unsafe 6 | - {name: unsafeFixIO, within: []} # Unsafe 7 | - {name: unsafePerformIO, within: []} # Unsafe 8 | 9 | # _VERY_ hard to get right, use the async library instead. 10 | # See also https://github.com/informatikr/hedis/issues/165 11 | - {name: forkIO, within: []} 12 | # Mostly impossible to get right, rethink what you're doing entirely. 13 | # See also https://www.reddit.com/r/haskell/comments/jsap9r/how_dangerous_is_forkprocess/ 14 | - {name: forkProcess, within: []} 15 | 16 | - {name: undefined, within: []} # Purposely fails. Deal with errors appropriately instead. 17 | - {name: throw, within: []} # Don't throw from pure code, use throwIO instead. 18 | - {name: Prelude.error, within: []} 19 | 20 | - {name: Data.List.head, within: []} # Partial, use `listToMaybe` instead. 21 | - {name: Data.List.tail, within: []} # Partial 22 | - {name: Data.List.init, within: []} # Partial 23 | - {name: Data.List.last, within: []} # Partial 24 | - {name: 'Data.List.!!', within: []} # Partial 25 | - {name: Data.List.genericIndex, within: []} # Partial 26 | 27 | # Same, but for Data.Text 28 | - {name: Data.Text.head, within: []} 29 | - {name: Data.Text.tail, within: []} 30 | - {name: Data.Text.init, within: []} 31 | - {name: Data.Text.last, within: []} 32 | 33 | - {name: minimum, within: []} # Partial 34 | - {name: minimumBy, within: []} # Partial 35 | - {name: maximum, within: []} # Partial 36 | - {name: maximumBy, within: []} # Partial 37 | 38 | # Same, but for Data.Text 39 | - {name: Data.Text.maximum, within: []} 40 | - {name: Data.Text.minimum, within: []} 41 | 42 | - {name: GHC.Enum.pred, within: []} # Partial 43 | - {name: GHC.Enum.suc, within: []} # Partial 44 | - {name: GHC.Enum.toEnum, within: []} # Partial 45 | - {name: GHC.Enum.fromEnum, within: []} # Does not do what you think it does. 46 | - {name: GHC.Enum.enumFrom, within: []} # Does not do what you think it does, depending on the type. 47 | - {name: GHC.Enum.enumFromThen, within: []} # Does not do what you think it does, depending on the type. 48 | - {name: GHC.Enum.enumFromTo, within: []} # Does not do what you think it does, depending on the type. 49 | - {name: GHC.Enum.enumFromThenTo, within: []} # Does not do what you think it does, depending on the type. 50 | 51 | - {name: unless, within: []} # Really confusing, use 'when' instead. 52 | # I've commented this out because, who hasn't memorized 53 | # `data Either a b = Left a | Right b`? Obviously it's left then right, so 54 | # the either function is `either handle_left handle_right either_value` 55 | # Just indent the 3 arguments so it resembles a case-match, like so: 56 | # a = either 57 | # (handle_left foo) 58 | # (handle_right bar) 59 | # (qux either_value) 60 | # Unlike a case-match, this function lets me write point-free functions for 61 | # handle_left and handle_right 62 | #- {name: either, within: []} # Really confusing, just use a case-match. 63 | 64 | - {name: nub, within: []} # O(n^2) 65 | 66 | - {name: foldl, within: []} # Lazy. Use foldl' instead. 67 | - {name: sum, within: []} # Lazy accumulator 68 | - {name: product, within: []} # Lazy accumulator 69 | 70 | # Functions involving division 71 | - {name: Prelude.quot, within: []} # Partial, see https://github.com/NorfairKing/haskell-WAT#num-int 72 | - {name: Prelude.div, within: []} 73 | - {name: Prelude.rem, within: []} 74 | - {name: Prelude.mod, within: []} 75 | - {name: Prelude.quotRem, within: []} 76 | - {name: Prelude.divMod, within: []} 77 | 78 | # Does unexpected things, see 79 | # https://github.com/NorfairKing/haskell-WAT#real-double 80 | - {name: realToFrac, within: []} 81 | 82 | # Don't use string for command-line output. 83 | - {name: System.IO.putChar, within: []} 84 | - {name: System.IO.putStr, within: []} 85 | - {name: System.IO.putStrLn, within: []} 86 | - {name: System.IO.print, within: []} 87 | 88 | # Don't use string for command-line input either. 89 | - {name: System.IO.getChar, within: []} 90 | - {name: System.IO.getLine, within: []} 91 | - {name: System.IO.getContents, within: []} # Does lazy IO. 92 | - {name: System.IO.interact, within: []} 93 | - {name: System.IO.readIO, within: []} 94 | - {name: System.IO.readLn, within: []} 95 | 96 | # Don't use strings to interact with files 97 | - {name: System.IO.readFile, within: []} 98 | - {name: System.IO.writeFile, within: []} 99 | - {name: System.IO.appendFile, within: []} 100 | 101 | # Can succeed in dev, but fail in prod, because of encoding guessing 102 | # It's also Lazy IO. 103 | # See https://www.snoyman.com/blog/2016/12/beware-of-readfile/ for more info. 104 | - {name: Data.Text.IO.readFile, within: []} 105 | - {name: Data.Text.IO.Lazy.readFile, within: []} 106 | 107 | - {name: Data.Text.Encoding.decodeUtf8, within: []} # Throws on invalid UTF8 108 | 109 | - {name: fromJust, within: []} # Partial 110 | 111 | # Does silent truncation: 112 | # > fromIntegral (300 :: Word) :: Word8 113 | # 44 114 | - {name: fromIntegral, within: []} 115 | 116 | 117 | - {name: 'read', within: []} # Partial, use `Text.Read.readMaybe` instead. 118 | 119 | # Deprecated, use `pure` instead. 120 | # See https://gitlab.haskell.org/ghc/ghc/-/wikis/proposal/monad-of-no-return 121 | - {name: 'return', within: []} 122 | 123 | - modules: 124 | - { name: Control.Lens, within: [] } 125 | 126 | - extensions: 127 | - { name: DeriveAnyClass, within: [] } # Dangerous 128 | 129 | - { name: DuplicateRecordFields, within: [] } 130 | 131 | - { name: NamedFieldPuns, within: [] } 132 | - { name: TupleSections, within: [] } 133 | - { name: OverloadedLabels, within: [] } 134 | 135 | -------------------------------------------------------------------------------- /.stylish-haskell.yaml: -------------------------------------------------------------------------------- 1 | # stylish-haskell configuration file 2 | # ================================== 3 | 4 | # The stylish-haskell tool is mainly configured by specifying steps. These steps 5 | # are a list, so they have an order, and one specific step may appear more than 6 | # once (if needed). Each file is processed by these steps in the given order. 7 | steps: 8 | # Convert some ASCII sequences to their Unicode equivalents. This is disabled 9 | # by default. 10 | # - unicode_syntax: 11 | # # In order to make this work, we also need to insert the UnicodeSyntax 12 | # # language pragma. If this flag is set to true, we insert it when it's 13 | # # not already present. You may want to disable it if you configure 14 | # # language extensions using some other method than pragmas. Default: 15 | # # true. 16 | # add_language_pragma: true 17 | 18 | # Format module header 19 | # 20 | # Currently, this option is not configurable and will format all exports and 21 | # module declarations to minimize diffs 22 | # 23 | # - module_header: 24 | # # How many spaces use for indentation in the module header. 25 | # indent: 4 26 | # 27 | # # Should export lists be sorted? Sorting is only performed within the 28 | # # export section, as delineated by Haddock comments. 29 | # sort: true 30 | # 31 | # # See `separate_lists` for the `imports` step. 32 | # separate_lists: true 33 | 34 | # Format record definitions. This is disabled by default. 35 | # 36 | # You can control the layout of record fields. The only rules that can't be configured 37 | # are these: 38 | # 39 | # - "|" is always aligned with "=" 40 | # - "," in fields is always aligned with "{" 41 | # - "}" is likewise always aligned with "{" 42 | # 43 | # - records: 44 | # # How to format equals sign between type constructor and data constructor. 45 | # # Possible values: 46 | # # - "same_line" -- leave "=" AND data constructor on the same line as the type constructor. 47 | # # - "indent N" -- insert a new line and N spaces from the beginning of the next line. 48 | # equals: "indent 2" 49 | # 50 | # # How to format first field of each record constructor. 51 | # # Possible values: 52 | # # - "same_line" -- "{" and first field goes on the same line as the data constructor. 53 | # # - "indent N" -- insert a new line and N spaces from the beginning of the data constructor 54 | # first_field: "indent 2" 55 | # 56 | # # How many spaces to insert between the column with "," and the beginning of the comment in the next line. 57 | # field_comment: 2 58 | # 59 | # # How many spaces to insert before "deriving" clause. Deriving clauses are always on separate lines. 60 | # deriving: 2 61 | # 62 | # # How many spaces to insert before "via" clause counted from indentation of deriving clause 63 | # # Possible values: 64 | # # - "same_line" -- "via" part goes on the same line as "deriving" keyword. 65 | # # - "indent N" -- insert a new line and N spaces from the beginning of "deriving" keyword. 66 | # via: "indent 2" 67 | # 68 | # # Sort typeclass names in the "deriving" list alphabetically. 69 | # sort_deriving: true 70 | # 71 | # # Wheter or not to break enums onto several lines 72 | # # 73 | # # Default: false 74 | # break_enums: false 75 | # 76 | # # Whether or not to break single constructor data types before `=` sign 77 | # # 78 | # # Default: true 79 | # break_single_constructors: true 80 | # 81 | # # Whether or not to curry constraints on function. 82 | # # 83 | # # E.g: @allValues :: Enum a => Bounded a => Proxy a -> [a]@ 84 | # # 85 | # # Instead of @allValues :: (Enum a, Bounded a) => Proxy a -> [a]@ 86 | # # 87 | # # Default: false 88 | # curried_context: false 89 | 90 | # Align the right hand side of some elements. This is quite conservative 91 | # and only applies to statements where each element occupies a single 92 | # line. 93 | # Possible values: 94 | # - always - Always align statements. 95 | # - adjacent - Align statements that are on adjacent lines in groups. 96 | # - never - Never align statements. 97 | # All default to always. 98 | - simple_align: 99 | cases: always 100 | top_level_patterns: always 101 | records: always 102 | multi_way_if: always 103 | 104 | # Import cleanup 105 | - imports: 106 | # There are different ways we can align names and lists. 107 | # 108 | # - global: Align the import names and import list throughout the entire 109 | # file. 110 | # 111 | # - file: Like global, but don't add padding when there are no qualified 112 | # imports in the file. 113 | # 114 | # - group: Only align the imports per group (a group is formed by adjacent 115 | # import lines). 116 | # 117 | # - none: Do not perform any alignment. 118 | # 119 | # Default: global. 120 | align: group 121 | 122 | # The following options affect only import list alignment. 123 | # 124 | # List align has following options: 125 | # 126 | # - after_alias: Import list is aligned with end of import including 127 | # 'as' and 'hiding' keywords. 128 | # 129 | # > import qualified Data.List as List (concat, foldl, foldr, head, 130 | # > init, last, length) 131 | # 132 | # - with_alias: Import list is aligned with start of alias or hiding. 133 | # 134 | # > import qualified Data.List as List (concat, foldl, foldr, head, 135 | # > init, last, length) 136 | # 137 | # - with_module_name: Import list is aligned `list_padding` spaces after 138 | # the module name. 139 | # 140 | # > import qualified Data.List as List (concat, foldl, foldr, head, 141 | # init, last, length) 142 | # 143 | # This is mainly intended for use with `pad_module_names: false`. 144 | # 145 | # > import qualified Data.List as List (concat, foldl, foldr, head, 146 | # init, last, length, scanl, scanr, take, drop, 147 | # sort, nub) 148 | # 149 | # - new_line: Import list starts always on new line. 150 | # 151 | # > import qualified Data.List as List 152 | # > (concat, foldl, foldr, head, init, last, length) 153 | # 154 | # - repeat: Repeat the module name to align the import list. 155 | # 156 | # > import qualified Data.List as List (concat, foldl, foldr, head) 157 | # > import qualified Data.List as List (init, last, length) 158 | # 159 | # Default: after_alias 160 | list_align: after_alias 161 | 162 | # Right-pad the module names to align imports in a group: 163 | # 164 | # - true: a little more readable 165 | # 166 | # > import qualified Data.List as List (concat, foldl, foldr, 167 | # > init, last, length) 168 | # > import qualified Data.List.Extra as List (concat, foldl, foldr, 169 | # > init, last, length) 170 | # 171 | # - false: diff-safe 172 | # 173 | # > import qualified Data.List as List (concat, foldl, foldr, init, 174 | # > last, length) 175 | # > import qualified Data.List.Extra as List (concat, foldl, foldr, 176 | # > init, last, length) 177 | # 178 | # Default: true 179 | pad_module_names: true 180 | 181 | # Long list align style takes effect when import is too long. This is 182 | # determined by 'columns' setting. 183 | # 184 | # - inline: This option will put as much specs on same line as possible. 185 | # 186 | # - new_line: Import list will start on new line. 187 | # 188 | # - new_line_multiline: Import list will start on new line when it's 189 | # short enough to fit to single line. Otherwise it'll be multiline. 190 | # 191 | # - multiline: One line per import list entry. 192 | # Type with constructor list acts like single import. 193 | # 194 | # > import qualified Data.Map as M 195 | # > ( empty 196 | # > , singleton 197 | # > , ... 198 | # > , delete 199 | # > ) 200 | # 201 | # Default: inline 202 | long_list_align: multiline 203 | 204 | # Align empty list (importing instances) 205 | # 206 | # Empty list align has following options 207 | # 208 | # - inherit: inherit list_align setting 209 | # 210 | # - right_after: () is right after the module name: 211 | # 212 | # > import Vector.Instances () 213 | # 214 | # Default: inherit 215 | empty_list_align: inherit 216 | 217 | # List padding determines indentation of import list on lines after import. 218 | # This option affects 'long_list_align'. 219 | # 220 | # - : constant value 221 | # 222 | # - module_name: align under start of module name. 223 | # Useful for 'file' and 'group' align settings. 224 | # 225 | # Default: 4 226 | list_padding: 4 227 | 228 | # Separate lists option affects formatting of import list for type 229 | # or class. The only difference is single space between type and list 230 | # of constructors, selectors and class functions. 231 | # 232 | # - true: There is single space between Foldable type and list of it's 233 | # functions. 234 | # 235 | # > import Data.Foldable (Foldable (fold, foldl, foldMap)) 236 | # 237 | # - false: There is no space between Foldable type and list of it's 238 | # functions. 239 | # 240 | # > import Data.Foldable (Foldable(fold, foldl, foldMap)) 241 | # 242 | # Default: true 243 | separate_lists: true 244 | 245 | # Space surround option affects formatting of import lists on a single 246 | # line. The only difference is single space after the initial 247 | # parenthesis and a single space before the terminal parenthesis. 248 | # 249 | # - true: There is single space associated with the enclosing 250 | # parenthesis. 251 | # 252 | # > import Data.Foo ( foo ) 253 | # 254 | # - false: There is no space associated with the enclosing parenthesis 255 | # 256 | # > import Data.Foo (foo) 257 | # 258 | # Default: false 259 | space_surround: false 260 | 261 | # Enabling this argument will use the new GHC lib parse to format imports. 262 | # 263 | # This currently assumes a few things, it will assume that you want post 264 | # qualified imports. It is also not as feature complete as the old 265 | # imports formatting. 266 | # 267 | # It does not remove redundant lines or merge lines. As such, the full 268 | # feature scope is still pending. 269 | # 270 | # It _is_ however, a fine alternative if you are using features that are 271 | # not parseable by haskell src extensions and you're comfortable with the 272 | # presets. 273 | # 274 | # Default: false 275 | ghc_lib_parser: false 276 | 277 | # Language pragmas 278 | - language_pragmas: 279 | # We can generate different styles of language pragma lists. 280 | # 281 | # - vertical: Vertical-spaced language pragmas, one per line. 282 | # 283 | # - compact: A more compact style. 284 | # 285 | # - compact_line: Similar to compact, but wrap each line with 286 | # `{-#LANGUAGE #-}'. 287 | # 288 | # Default: vertical. 289 | style: vertical 290 | 291 | # Align affects alignment of closing pragma brackets. 292 | # 293 | # - true: Brackets are aligned in same column. 294 | # 295 | # - false: Brackets are not aligned together. There is only one space 296 | # between actual import and closing bracket. 297 | # 298 | # Default: true 299 | align: true 300 | 301 | # stylish-haskell can detect redundancy of some language pragmas. If this 302 | # is set to true, it will remove those redundant pragmas. Default: true. 303 | remove_redundant: true 304 | 305 | # Language prefix to be used for pragma declaration, this allows you to 306 | # use other options non case-sensitive like "language" or "Language". 307 | # If a non correct String is provided, it will default to: LANGUAGE. 308 | language_prefix: LANGUAGE 309 | 310 | # Replace tabs by spaces. This is disabled by default. 311 | # - tabs: 312 | # # Number of spaces to use for each tab. Default: 8, as specified by the 313 | # # Haskell report. 314 | # spaces: 8 315 | 316 | # Remove trailing whitespace 317 | - trailing_whitespace: {} 318 | 319 | # Squash multiple spaces between the left and right hand sides of some 320 | # elements into single spaces. Basically, this undoes the effect of 321 | # simple_align but is a bit less conservative. 322 | # - squash: {} 323 | 324 | # A common setting is the number of columns (parts of) code will be wrapped 325 | # to. Different steps take this into account. 326 | # 327 | # Set this to null to disable all line wrapping. 328 | # 329 | # Default: 80. 330 | columns: 80 331 | 332 | # By default, line endings are converted according to the OS. You can override 333 | # preferred format here. 334 | # 335 | # - native: Native newline format. CRLF on Windows, LF on other OSes. 336 | # 337 | # - lf: Convert to LF ("\n"). 338 | # 339 | # - crlf: Convert to CRLF ("\r\n"). 340 | # 341 | # Default: native. 342 | newline: native 343 | 344 | # Sometimes, language extensions are specified in a cabal file or from the 345 | # command line instead of using language pragmas in the file. stylish-haskell 346 | # needs to be aware of these, so it can parse the file correctly. 347 | # 348 | # No language extensions are enabled by default. 349 | # language_extensions: 350 | # - TemplateHaskell 351 | # - QuasiQuotes 352 | 353 | # Attempt to find the cabal file in ancestors of the current directory, and 354 | # parse options (currently only language extensions) from that. 355 | # 356 | # Default: true 357 | cabal: true 358 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | none: 2 | $(error Run either `make build` or `make install`) 3 | 4 | build: 5 | cd docs && make theme 6 | stack build 7 | 8 | install: 9 | cd docs && make theme 10 | stack install 11 | 12 | clean: 13 | $(warning WARNING: docs/out/ SHOULD be committed to the repo) 14 | $(warning because it provides online documentation) 15 | $(warning Please do not immediately commit; run `make docs` first) 16 | $(warning Only use to test the above commands) 17 | rm -r docs/out/ 18 | 19 | .PHONY: docs 20 | docs: 21 | cd docs && make 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # kpxhs 2 | 3 | [![commits since](https://img.shields.io/github/commits-since/akazukin5151/kpxhs/latest)](https://GitHub.com/akazukin5151/kpxhs/commit/) [![status](https://img.shields.io/badge/status-active-green)](https://img.shields.io/badge/status-active-green) 4 | 5 | [Keepass](https://keepass.info/) database interactive TUI viewer based on `keepassxc-cli` 6 | 7 | ![unlock](pics/unlock.png) 8 | 9 | ![browser1](pics/browser1.png) 10 | 11 | ![countdown](pics/countdown.png) 12 | 13 | ![footer](pics/responsive_footer.png) 14 | 15 | ![entry](pics/entry.png) 16 | 17 | ![searching](pics/searching.png) 18 | 19 | ![browser2](pics/browser2.png) 20 | 21 | ![clear_clip](pics/clear_clip.png) 22 | 23 | # Features 24 | - Configurable database and keyfile path 25 | - Browse entries and navigate directories 26 | - View entry details 27 | - Copy entry username and password fields to clipboard 28 | - Clear clipboard on exit 29 | - Clear clipboard after a configurable timeout 30 | - Responsive messages in the footer 31 | 32 | # Why 33 | - Keyboard friendly way of accessing your passwords 34 | - Fast(er) 35 | - No need to type your password for every command, unlike `keepassxc-cli` 36 | - Password is cached for the entire session (make sure to close it after you're done) 37 | - Entries details are cached 38 | - Browser plugin doesn't work consistently for me 39 | - View only (for now?) because I access passwords much more often than I add or edit them. In rare cases when I have to, using the mouse and GUI isn't such a hassle. 40 | - Learn Haskell 41 | - I use arch btw 42 | 43 | \* If you want non-interactive (for scripting etc), just use keepassxc-cli directly 44 | 45 | 46 | # Usage requirements 47 | You need to install [keepassxc](https://github.com/keepassxreboot/keepassxc/) with `keepassxc-cli` and have it available in PATH. Versions 2.6.4 to 2.7.9 are known to work, but previous versions should still work if they're not too old. 48 | 49 | # Installing 50 | 51 | ## Install using pre-compiled binary 52 | 53 | Just go to the [releases](https://github.com/akazukin5151/kpxhs/releases/) page and grab the latest binary for your OS. Only UNIX (linux and macos) is supported. Binaries are compiled and uploaded using Github actions 54 | 55 | ## Build from source using [Stack](https://docs.haskellstack.org/en/stable/README/) 56 | 57 | No superuser permissions are needed (unless if you need to install stack itself) 58 | 59 | 1. `git clone https://github.com/akazukin5151/kpxhs` 60 | 2. `cd kpxhs` 61 | 3. Optional: `git checkout` your desired stable version tag (otherwise you will be using the unstable master branch) 62 | 4. `make build` (compile) 63 | 5. `make install` (move binary to `~/.local/bin/`) 64 | 65 | The binary size can be reduced by enabling dynamic linking: add the`-dynamic` flag to `ghc-options` in [kpxhs.cabal](kpxhs.cabal). 66 | 67 | # Manual (Usage, Configuration, Theming) 68 | 69 | ```sh 70 | $ kpxhs -h 71 | kpxhs - Interactive Keepass database TUI viewer 72 | Usage 73 | kpxhs Start the program 74 | kpxhs [-v | --version] Print the version number 75 | kpxhs [-h | --help] Show this help 76 | kpxhs --write-config Write the default configs to ~/.config/kpxhs/ 77 | 78 | TUI keybindings (in general) 79 | Esc Quit, back (elsewhere) 80 | q Quit, back (in browser) 81 | Tab Cycle focus 82 | / Search (filter) items 83 | Enter Show entry details 84 | u Copy username 85 | p Copy password 86 | 87 | Navigation ([n] means optional digit) 88 | [n]j, [n]s Move down n items (default: 1) 89 | [n]k, [n]w Move up n items (default: 1) 90 | g Move to top 91 | G Move to bottom 92 | ``` 93 | 94 | Read the man page for more details. The rendered markdown is available online at https://github.com/akazukin5151/kpxhs/blob/master/docs/out/kpxhs.1.md 95 | 96 | Alternatively, it can be installed for offline reading manually. No superuser permissions are required. Copy the pre-built man page in [docs/out/kpxhs.1](https://github.com/akazukin5151/kpxhs/raw/master/docs/out/kpxhs.1) to somewhere in your `manpath` (eg, `~/.local/share/man/man1/`). View with `man kpxhs`. 97 | 98 | The man page can also be manually built with `cd docs && make` and installed with `make copy`. `pandoc` is required for this manual build. For the `whatis` program to work with `kpxhs`, refresh the mandb with `mandb` after `make copy` 99 | 100 | # License 101 | 102 | GPLv3 or later 103 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | /home/twenty/Programming/kpxhs/docs/RELEASE.md -------------------------------------------------------------------------------- /Setup.hs: -------------------------------------------------------------------------------- 1 | import Distribution.Simple 2 | main = defaultMain 3 | -------------------------------------------------------------------------------- /Theming.md: -------------------------------------------------------------------------------- 1 | # Theming 2 | 3 | This file is now documentation for other developers, in particular it justifies design decisions, evaluates its pros and cons, and discusses security measures. 4 | 5 | Read the user guide in the manual here: https://github.com/akazukin5151/kpxhs/blob/master/docs/out/kpxhs.1.md 6 | 7 | ## Reasons for design decisions 8 | 9 | - The colors/theme is usually specified (hardcoded) like: 10 | 11 | ```hs 12 | A.attrMap V.defAttr [ (attrName1, attr1), (attrName2, attr2) ] 13 | ``` 14 | [See also: Brick docs on](https://hackage.haskell.org/package/brick-0.64/docs/Brick-AttrMap.html) `AttrMap` 15 | 16 | - The idea is to move that list-of-tuples into a file to be read and evaluated at launch 17 | - The goals are **flexibility of theming, minimal internal processing, and to avoid extra dependencies or use of another configuration language** 18 | - There are several advantages in using a Haskell expression as a config and theme file: 19 | - It can just be `read` natively, no parsing needed, no need to have Haskell installed 20 | - No need to build a DSL 21 | - Aeson and Dhall doubled and tripled the binary size respectively 22 | - Dhall might require the user to install dhall binaries or tooling 23 | - Anything that `A.attrMap V.defAttr` accepts is valid, maintaining full flexibility. 24 | - [Brick.Themes](https://hackage.haskell.org/package/brick-0.64/docs/Brick-Themes.html) are not flexible enough. For example, a default must be given to the function `newTheme`. Themes must provide a default attribute or get the exception "serializeCustomColor does not support KeepCurrent". It distinguishes between a customization and a theme but I want everything to be customizable. 25 | 26 | - There are however a few disadvantages: 27 | - The config and theme files are fragile and failure intolerant. Although Haskell syntax is relatively lenient for expressions, any error will cause a fallback to the default, even if just one small part cannot be parsed. 28 | - If parsing fails, it doesn't let the user know where the error is 29 | - Requires some knowledge of Haskell to be fully confident in editing. 30 | - Enums are used extensively anyway (the list of valid colors and styles), so having a dhall-like tooling and type checking is better than nothing. 31 | - But essentially only applies to Dhall, because JSON and YAML cannot type check your enum variants anyway. How many linux terminal utilities use Dhall for their configs? A vast majority of them relies on textually listing out the valid values anyway. 32 | - Actually, there is one prominent Haskell program on linux that uses Haskell for configuration - XMonad. And it actually uses a proper Haskell module, so it's not like it's unprecended. 33 | 34 | ## Security 35 | 36 | While it may seem insecure to evaluate a raw Haskell file, it cannot contain any functions, and it has to type check as the type `UserFacingTheme`. That means there's no way to cheat and successfully pass in something that's not `UserFacingTheme`. For example, writing `unsafePerformIO (writeFile "log" "boom")` does not work because two functions are used here. No matter where `unsafePerformIO` is placed in the list-of-tuples, it can't be evaluated. There's no mechanism in the kpxhs source to evaluate arbitrary functions, only {fore, back}ground colors and text styles. Colors must type check as `Color`, and text styles type check as `Style`. *As long as Haskell's read function does not evaluate and execute functions, it is secure* 37 | 38 | It is Turing incomplete because `read` is Turing incomplete. This means parsing and evaluation is guaranteed to terminate. 39 | 40 | Is it possible for an update to expose a vulnerability? Yes, either by maliciously or accidentally. But as with any FOSS software, you can always review the source code or at least the changes yourself. It doesn't auto-update, and is not in any package repositories where an update can sneak in, so every update has to be installed manually. If you find a security flaw, please do report it. 41 | 42 | PS: no superuser permissions are required for the *binary installation* as well as usage. Installing stack probably requires it, but not compilation (`stack build` *and* `stack install`). 43 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Run `make` to convert md into man; `make copy` to copy that 2 | # file to your local manual dir (no superuser needed) 3 | 4 | build: theme 5 | cat src/part-1.md out/default_theme.hs src/part-2.md > out/kpxhs.1.md 6 | pandoc --standalone --to man out/kpxhs.1.md -o out/kpxhs.1 7 | 8 | copy: 9 | mkdir -p ~/.local/share/man/man1/ 10 | cp out/kpxhs.1 ~/.local/share/man/man1/kpxhs.1 11 | # note that mandb might need to be refreshed, eg for `whatis` to work 12 | # just run `mandb` 13 | # commented out because this is a copy command, not an install command 14 | 15 | theme: 16 | python3 src/section.py 17 | -------------------------------------------------------------------------------- /docs/RELEASE.md: -------------------------------------------------------------------------------- 1 | # How to make a new release 2 | 3 | 1. Bump version number in `kpxhs.cabal`, `src/kpxhs/Constants.hs`, and `docs/src/part-1.md` 4 | 2. Run `make` in the `docs` dir 5 | 3. Push to Github and inspect CI logs 6 | 4. Add a release tag and push to Github 7 | -------------------------------------------------------------------------------- /docs/example_config.hs: -------------------------------------------------------------------------------- 1 | Config { timeout = Just (Seconds 10) 2 | , dbPath = Just "/home/me/kpxhs/test/kpxhs_test.kdbx" 3 | , keyfilePath = Just "/home/me/kpxhs/test/keyfile.key" 4 | , focusSearchOnStart = Just False 5 | } 6 | -------------------------------------------------------------------------------- /docs/out/default_theme.hs: -------------------------------------------------------------------------------- 1 | [ (["edit"], Val { fg = Black, bg = White, styles = [] }) 2 | , (["edit","focused"], Val { fg = White, bg = Blue, styles = [] }) 3 | , (["dialog"], Val { fg = White, bg = Blue, styles = [] }) 4 | , (["button"], Val { fg = Black, bg = White, styles = [] }) 5 | , (["button","selected"], Val { fg = Def, bg = Yellow, styles = [] }) 6 | , (["progressComplete"], Val { fg = White, bg = Blue, styles = [] }) 7 | , (["kpxhs","key"], Val { fg = Def, bg = White, styles = [] }) 8 | , (["kpxhs","label"], Val { fg = Black, bg = Def, styles = [] }) 9 | , (["kpxhs","line_number"], Val { fg = Yellow, bg = Def, styles = [] }) 10 | , (["kpxhs","line_number","focused"], Val { fg = Red, bg = Def, styles = [Bold]}) 11 | , (["kpxhs","list_border"], Val { fg = Black, bg = Def, styles = [] }) 12 | , (["kpxhs","list_border","focused"], Val { fg = Blue, bg = Def, styles = [] }) 13 | , (["kpxhs","directory"], Val { fg = Black, bg = Def, styles = [Bold]}) 14 | , (["kpxhs","directory","focused"], Val { fg = Red, bg = Def, styles = [Bold]}) 15 | , (["kpxhs","go_up"], Val { fg = Green 16 | , bg = Def 17 | , styles = [Bold, Italic] 18 | }) 19 | , (["kpxhs","go_up","focused"], Val { fg = Blue 20 | , bg = Def 21 | , styles = [Bold, Italic] 22 | }) 23 | , (["kpxhs","entry"], Val { fg = Black, bg = Def, styles = [] }) 24 | , (["kpxhs","entry","focused"], Val { fg = Red, bg = Def, styles = [] }) 25 | ] 26 | -------------------------------------------------------------------------------- /docs/out/kpxhs.1: -------------------------------------------------------------------------------- 1 | '\" t 2 | .\" Automatically generated by Pandoc 3.0.1 3 | .\" 4 | .\" Define V font for inline verbatim, using C font in formats 5 | .\" that render this, and otherwise B font. 6 | .ie "\f[CB]x\f[]"x" \{\ 7 | . ftr V B 8 | . ftr VI BI 9 | . ftr VB B 10 | . ftr VBI BI 11 | .\} 12 | .el \{\ 13 | . ftr V CR 14 | . ftr VI CI 15 | . ftr VB CB 16 | . ftr VBI CBI 17 | .\} 18 | .TH "kpxhs" "1" "" "Version 1.11" "kpxhs manual" 19 | .hy 20 | .SH NAME 21 | .PP 22 | \f[B]kpxhs\f[R] - Interactive Keepass database TUI viewer 23 | .SH SYNOPSIS 24 | .PP 25 | \f[B]kpxhs\f[R] [-h | --help] [-v | --version] [--write-config] 26 | .SH DESCRIPTION 27 | .PP 28 | Interactive Keepass (https://keepass.info/) database TUI viewer. 29 | The database and keyfile path can be configured to auto-fill on launch. 30 | Entry details can be viewed and their username and password fields can 31 | be copied to clipboard. 32 | On exit, if the clipboard is \[lq]dirty\[rq] then it will offer to clear 33 | it. 34 | The clipboard can also be cleared after a configurable timeout, along 35 | with a progress bar that counts down to clear. 36 | .SH OPTIONS 37 | .TP 38 | \f[V]h, help\f[R] 39 | Prints the help message and exit. 40 | Ignores number of dashes. 41 | .TP 42 | \f[V]v, version\f[R] 43 | Prints the version number and exit. 44 | Ignores number of dashes. 45 | .TP 46 | \f[V]write-config\f[R] 47 | Writes the default config.hs and theme.hs files to \[ti]/.config/kpxhs/. 48 | Ignores number of dashes. 49 | .SH KEYBINDINGS 50 | .TP 51 | \f[V]q\f[R] 52 | Go up a directory, or attempt to quit if in root directory. 53 | .TP 54 | \f[V]Esc\f[R] 55 | Focus or go back to browser (from entry details or search) 56 | .TP 57 | \f[V]Tab, Shift-Tab\f[R] 58 | Cycle focus between elements in login fields and exit dialog 59 | .TP 60 | \f[V]Enter\f[R] 61 | Show entry details of selected entry. 62 | Attempts login if locked 63 | .TP 64 | \f[V]u\f[R] 65 | Copy username of selected/current entry 66 | .TP 67 | \f[V]p\f[R] 68 | Copy password of selected/current entry 69 | .TP 70 | \f[V][n]j, [n]s\f[R] 71 | Move down the list by \[ga]n\[ga] items, default is 1. 72 | \[ga]n\[ga] is an optional series of digits (0-9), that terminates when 73 | \[ga]j\[ga] or \[ga]s\[ga] is pressed. 74 | This resembles the vim motion commands. 75 | Leading zeros are stripped. 76 | For example, \[ga]4j\[ga] will move four items down and \[ga]10j\[ga] 77 | will move ten items down. 78 | Use the relative line numbers to help; the number shown is the exact 79 | number needed for the digit \[ga]n\[ga] 80 | .TP 81 | \f[V][n]k, [n]w\f[R] 82 | Move up the list by \[ga]n\[ga] items, default is 1. 83 | \[ga]n\[ga] is an optional series of digits (0-9), that terminates when 84 | \[ga]k\[ga] or \[ga]w\[ga] is pressed. 85 | Leading zeros are stripped. 86 | This resembles the vim motion commands. 87 | For example, \[ga]4k\[ga] will move four items down and \[ga]10k\[ga] 88 | will move ten items down. 89 | Use the relative line numbers to help; the number shown is the exact 90 | number needed for the digit \[ga]n\[ga] 91 | .TP 92 | \f[V]g\f[R] 93 | Move to the top of the list 94 | .TP 95 | \f[V]G\f[R] 96 | Move to the bottom of the list 97 | .PP 98 | The following table shows a summary of the keybindings and their effects 99 | in each mode 100 | .PP 101 | .TS 102 | tab(@); 103 | lw(4.6n) lw(17.5n) lw(12.9n) lw(12.9n) lw(11.1n) lw(11.1n). 104 | T{ 105 | Key 106 | T}@T{ 107 | Browser 108 | T}@T{ 109 | Search 110 | T}@T{ 111 | Entry details 112 | T}@T{ 113 | Login 114 | T}@T{ 115 | Exit dialog 116 | T} 117 | _ 118 | T{ 119 | q 120 | T}@T{ 121 | Go up dir or quit 122 | T}@T{ 123 | - 124 | T}@T{ 125 | - 126 | T}@T{ 127 | - 128 | T}@T{ 129 | - 130 | T} 131 | T{ 132 | Esc 133 | T}@T{ 134 | Clear command 135 | T}@T{ 136 | Focus Browser 137 | T}@T{ 138 | Back 139 | T}@T{ 140 | Quit 141 | T}@T{ 142 | - 143 | T} 144 | T{ 145 | / 146 | T}@T{ 147 | Focus Search 148 | T}@T{ 149 | - 150 | T}@T{ 151 | - 152 | T}@T{ 153 | - 154 | T}@T{ 155 | - 156 | T} 157 | T{ 158 | Tab 159 | T}@T{ 160 | - 161 | T}@T{ 162 | - 163 | T}@T{ 164 | - 165 | T}@T{ 166 | Cycle Focus 167 | T}@T{ 168 | Cycle Focus 169 | T} 170 | T{ 171 | Enter 172 | T}@T{ 173 | Show details 174 | T}@T{ 175 | - 176 | T}@T{ 177 | - 178 | T}@T{ 179 | Unlock 180 | T}@T{ 181 | - 182 | T} 183 | T{ 184 | j 185 | T}@T{ 186 | Move down 187 | T}@T{ 188 | - 189 | T}@T{ 190 | - 191 | T}@T{ 192 | - 193 | T}@T{ 194 | - 195 | T} 196 | T{ 197 | k 198 | T}@T{ 199 | Move up 200 | T}@T{ 201 | - 202 | T}@T{ 203 | - 204 | T}@T{ 205 | - 206 | T}@T{ 207 | - 208 | T} 209 | T{ 210 | u 211 | T}@T{ 212 | Copy username 213 | T}@T{ 214 | - 215 | T}@T{ 216 | - 217 | T}@T{ 218 | - 219 | T}@T{ 220 | - 221 | T} 222 | T{ 223 | p 224 | T}@T{ 225 | Copy password 226 | T}@T{ 227 | - 228 | T}@T{ 229 | - 230 | T}@T{ 231 | - 232 | T}@T{ 233 | - 234 | T} 235 | T{ 236 | g 237 | T}@T{ 238 | Go to top 239 | T}@T{ 240 | - 241 | T}@T{ 242 | - 243 | T}@T{ 244 | - 245 | T}@T{ 246 | - 247 | T} 248 | T{ 249 | G 250 | T}@T{ 251 | Go to bottom 252 | T}@T{ 253 | - 254 | T}@T{ 255 | - 256 | T}@T{ 257 | - 258 | T}@T{ 259 | - 260 | T} 261 | .TE 262 | .SH EXAMPLE USAGE 263 | .IP "1." 3 264 | \[ga]kpxhs\[ga] 265 | .IP "2." 3 266 | \[ga]YOUR_PASSWORD\[ga] (Assuming database path stored in config and no 267 | keyfile) 268 | .IP "3." 3 269 | \[ga]/\[ga] (Focus search bar) 270 | .IP "4." 3 271 | \[ga]git\[ga] (Filter list to items with \[lq]git\[rq] in title) 272 | .IP "5." 3 273 | \[ga]Esc\[ga] (Focus to list) 274 | .IP "6." 3 275 | \[ga]j\[ga] (Focus one entry below) 276 | .IP "7." 3 277 | \[ga]p\[ga] (Copy password) 278 | .IP "8." 3 279 | \[ga]Esc\[ga] (quit) 280 | .IP "9." 3 281 | (Focus is on clear clipboard and exit by default) \[ga]Enter\[ga] (clear 282 | clipboard and exit) 283 | .SH CONFIGURATION 284 | .SS INTRODUCTION 285 | .PP 286 | You can set the database and keyfile fields to be auto-filled with any 287 | path, so you only need to enter your password on launch. 288 | You can also customize the automatic clipboard clearing: change the 289 | number of seconds to wait before clearing the clipboard; or disable the 290 | feature altogether. 291 | .PP 292 | Hint: run \[ga]kpxhs \[en]write-config\[ga] to generate the default 293 | config and theme for further editing 294 | .SS SETTINGS 295 | .PP 296 | The config file is located in \[ga]\[ti]/.config/kpxhs/config.hs\[ga]. 297 | Make sure it is encoded as UTF-8. 298 | Write something like: 299 | .IP 300 | .nf 301 | \f[C] 302 | Config { timeout = Just (Seconds 10) 303 | , dbPath = Just \[dq]/home/me/kpxhs/test/kpxhs_test.kdbx\[dq] 304 | , keyfilePath = Just \[dq]/home/me/kpxhs/test/keyfile.key\[dq] 305 | , focusSearchOnStart = Just False 306 | } 307 | \f[R] 308 | .fi 309 | .PP 310 | \f[B]It must be a valid Haskell expression\f[R] of a record with four 311 | fields: timeout, dbPath, keyfilePath, and focusSearchOnStart. 312 | All three are Maybe types - they are optional and you can always omit 313 | specifying them by writing \[ga]Nothing\[ga]. 314 | Do not delete a field however, as it will result in an invalid config. 315 | .PP 316 | The paths can be any UTF-8 string; no validation is performed on them. 317 | .SS timeout 318 | .PP 319 | After copying a username or password, \f[I]kpxhs\f[R] can automatically 320 | clear the clipboard after a set number of seconds. 321 | There are three valid values for the timeout field: 322 | .TP 323 | \f[V]Just (Seconds t)\f[R] 324 | Set the number of seconds to wait for \[ga]t\[ga] seconds after the copy 325 | for clearing the clipboard. 326 | The number of seconds must be an integer. 327 | .TP 328 | \f[V]Just DoNotClear\f[R] 329 | Disable automatic clipboard clearing. 330 | .TP 331 | \f[V]Nothing\f[R] 332 | Fall back to the default, which is \f[B]10 seconds\f[R] 333 | .SS dbPath 334 | .PP 335 | \f[I]kpxhs\f[R] can auto-fill the database path with a given string. 336 | There are two valid values: 337 | .TP 338 | \f[V]Just \[dq]xxx\[dq]\f[R] 339 | Fill in the database path with the string \[lq]xxx\[rq], without quotes. 340 | .TP 341 | \f[V]Nothing\f[R] 342 | Fall back to the default, which is the empty string \[lq]\[rq] 343 | .SS keyfilePath 344 | .PP 345 | \f[I]kpxhs\f[R] can auto-fill the keyfile path with a given string. 346 | There are two valid values: 347 | .TP 348 | \f[V]Just \[dq]xxx\[dq]\f[R] 349 | Fill in the keyfile path with the string \[lq]xxx\[rq], without quotes. 350 | .TP 351 | \f[V]Nothing\f[R] 352 | Fall back to the default, which is the empty string \[lq]\[rq] 353 | .SS focusSearchOnStart 354 | .PP 355 | Whether to focus the search bar after initial login, so \f[V]/\f[R] key 356 | doesn\[cq]t have to be pressed to focus it. 357 | There are two valid values: 358 | .TP 359 | \f[V]Just True\f[R] 360 | Focus the search bar after initial login 361 | .TP 362 | \f[V]Just False\f[R] 363 | Focus the browser list after initial login 364 | .TP 365 | \f[V]Nothing\f[R] 366 | Fall back to the default, which is to focus the browser list after 367 | initial login 368 | .SS THEMING 369 | .PP 370 | The theme file is located in \[ga]\[ti]/.config/kpxhs/theme.hs\[ga]. 371 | Make sure it is encoded as UTF-8. 372 | You should probably edit the default theme instead of writing from 373 | scratch, because if you write from scratch, all the colors in the 374 | default theme are lost. 375 | .PP 376 | This is the default theme if you don\[cq]t provide any: 377 | .IP 378 | .nf 379 | \f[C] 380 | [ ([\[dq]edit\[dq]], Val { fg = Black, bg = White, styles = [] }) 381 | , ([\[dq]edit\[dq],\[dq]focused\[dq]], Val { fg = White, bg = Blue, styles = [] }) 382 | , ([\[dq]dialog\[dq]], Val { fg = White, bg = Blue, styles = [] }) 383 | , ([\[dq]button\[dq]], Val { fg = Black, bg = White, styles = [] }) 384 | , ([\[dq]button\[dq],\[dq]selected\[dq]], Val { fg = Def, bg = Yellow, styles = [] }) 385 | , ([\[dq]progressComplete\[dq]], Val { fg = White, bg = Blue, styles = [] }) 386 | , ([\[dq]kpxhs\[dq],\[dq]key\[dq]], Val { fg = Def, bg = White, styles = [] }) 387 | , ([\[dq]kpxhs\[dq],\[dq]label\[dq]], Val { fg = Black, bg = Def, styles = [] }) 388 | , ([\[dq]kpxhs\[dq],\[dq]line_number\[dq]], Val { fg = Yellow, bg = Def, styles = [] }) 389 | , ([\[dq]kpxhs\[dq],\[dq]line_number\[dq],\[dq]focused\[dq]], Val { fg = Red, bg = Def, styles = [Bold]}) 390 | , ([\[dq]kpxhs\[dq],\[dq]list_border\[dq]], Val { fg = Black, bg = Def, styles = [] }) 391 | , ([\[dq]kpxhs\[dq],\[dq]list_border\[dq],\[dq]focused\[dq]], Val { fg = Blue, bg = Def, styles = [] }) 392 | , ([\[dq]kpxhs\[dq],\[dq]directory\[dq]], Val { fg = Black, bg = Def, styles = [Bold]}) 393 | , ([\[dq]kpxhs\[dq],\[dq]directory\[dq],\[dq]focused\[dq]], Val { fg = Red, bg = Def, styles = [Bold]}) 394 | , ([\[dq]kpxhs\[dq],\[dq]go_up\[dq]], Val { fg = Green 395 | , bg = Def 396 | , styles = [Bold, Italic] 397 | }) 398 | , ([\[dq]kpxhs\[dq],\[dq]go_up\[dq],\[dq]focused\[dq]], Val { fg = Blue 399 | , bg = Def 400 | , styles = [Bold, Italic] 401 | }) 402 | , ([\[dq]kpxhs\[dq],\[dq]entry\[dq]], Val { fg = Black, bg = Def, styles = [] }) 403 | , ([\[dq]kpxhs\[dq],\[dq]entry\[dq],\[dq]focused\[dq]], Val { fg = Red, bg = Def, styles = [] }) 404 | ] 405 | \f[R] 406 | .fi 407 | .PP 408 | \f[B]The theme file must be a valid Haskell expression\f[R]. 409 | It is a list-of-2-tuples; for every tuple, the first item is a 410 | list-of-strings representing the attribute name, and the second item is 411 | the attribute value. 412 | The attribute value is represented as a record with three fields: fg, 413 | bg, and styles. 414 | The fg and bg fields only accept certain color names. 415 | styles is a list-of-styles, and also only accept certain style names. 416 | .SS Attribute names 417 | .TP 418 | \f[V][\[dq]xxx\[dq], \[dq]yyy\[dq]]\f[R] 419 | Represents an attribute name \[lq]xxx.yyy\[rq]. 420 | Must have at least one item. 421 | .PP 422 | There are a few special attribute names exclusive to \f[I]kpxhs\f[R]. 423 | They are appropriately namespaced with \[ga]\[lq]kpxhs\[rq]\[ga]. 424 | .TP 425 | \f[V][\[dq]kpxhs\[dq], \[dq]key\[dq]]\f[R] 426 | The key being bound (eg, \[lq]Esc\[rq]) 427 | .TP 428 | \f[V][\[dq]kpxhs\[dq], \[dq]label\[dq]]\f[R] 429 | The label bound (eg, \[lq]exit\[rq]) 430 | .PP 431 | In other words, the footer shows a nano-like grid of keys and their 432 | action. 433 | For example, \[lq]Esc exit\[rq] to indicate that pressing the Esc key 434 | will exit. 435 | \[ga]kpxhs.key\[ga] would style the \[lq]Esc\[rq] text and 436 | \[ga]kpxhs.label\[ga] would style the \[lq]exit\[rq] text 437 | .TP 438 | \f[V][\[dq]kpxhs\[dq], \[dq]line_number\[dq]]\f[R] 439 | The relative line numbers on the left side of the list, for entries that 440 | are not selected/in focus 441 | .TP 442 | \f[V][\[dq]kpxhs\[dq], \[dq]line_number\[dq], \[dq]focused\[dq]]\f[R] 443 | The relative line numbers on the left side of the list for the currently 444 | selected entry 445 | .TP 446 | \f[V][\[dq]kpxhs\[dq], \[dq]list_border\[dq]]\f[R] 447 | The list/browser border when it is not focused (ie, focus is on search 448 | bar). 449 | Only foreground color is used. 450 | .TP 451 | \f[V][\[dq]kpxhs\[dq], \[dq]list_border\[dq], \[dq]focused\[dq]]\f[R] 452 | The list/browser border when it is focused (ie, focus is on list). 453 | Only foreground color is used. 454 | .TP 455 | \f[V][\[dq]kpxhs\[dq], \[dq]directory\[dq]]\f[R] 456 | A directory that is not currently selected 457 | .TP 458 | \f[V][\[dq]kpxhs\[dq], \[dq]directory\[dq], \[dq]focused\[dq]]\f[R] 459 | A directory that is currently selected 460 | .TP 461 | \f[V][\[dq]kpxhs\[dq], \[dq]go_up\[dq]]\f[R] 462 | The \[lq]-- (Go up directory) --\[rq] text when it is not 463 | focused/selected 464 | .TP 465 | \f[V][\[dq]kpxhs\[dq], \[dq]go_up\[dq], \[dq]focused\[dq]]\f[R] 466 | The \[lq]-- (Go up directory) --\[rq] text when it is focused/selected 467 | .TP 468 | \f[V][\[dq]kpxhs\[dq], \[dq]entry\[dq]]\f[R] 469 | An entry that is not currently selected 470 | .TP 471 | \f[V][\[dq]kpxhs\[dq], \[dq]entry\[dq], \[dq]focused\[dq]]\f[R] 472 | An entry that is currently selected 473 | .PP 474 | Apart from those, you can use any other attribute name of elements used 475 | in the program. 476 | Here are the Brick docs for the attribute names of the elements used in 477 | \f[I]kpxhs\f[R]: 478 | .IP \[bu] 2 479 | List 480 | widget (https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-List.html#g:7) 481 | .IP \[bu] 2 482 | Exit 483 | dialog (https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-Dialog.html#g:4) 484 | .IP \[bu] 2 485 | Login 486 | dialog (https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-Edit.html#g:7) 487 | .IP \[bu] 2 488 | Progress 489 | bar (https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-ProgressBar.html#g:1) 490 | .IP \[bu] 2 491 | Borders (https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-Border.html#g:5) 492 | .SS Attribute values 493 | .PP 494 | The record has three fields: 495 | .TP 496 | \f[V]fg\f[R] 497 | Set the foreground color. 498 | See \f[B]Colors\f[R] 499 | .TP 500 | \f[V]bg\f[R] 501 | Set the background color. 502 | See \f[B]Colors\f[R] 503 | .TP 504 | \f[V]styles\f[R] 505 | Set the given styles. 506 | See \f[B]Styles\f[R] 507 | .SS Colors 508 | .TP 509 | \f[V]Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, BrightBlack, BrightRed, BrightGreen, BrightYellow, BrightBlue, BrightMagenta, BrightCyan, BrightWhite\f[R] 510 | Set the color to one of those 16 colors. 511 | Their exact values are configured through your terminal 512 | .TP 513 | \f[V]Def\f[R] 514 | Use the default color for that element. 515 | Essentially means a color is not specified 516 | .TP 517 | \f[V]RGB r g b\f[R] 518 | Use an RGB color given by the three integers, from 0 to 255 inclusive. 519 | Note that Brick doesn\[cq]t support the entire rgb palette, so some 520 | colors can throw an error. 521 | \f[I]kpxhs\f[R] allows it to be thrown, because some attributes might be 522 | a hassle to navigate to, so aborting the program will let the user know 523 | their color is invalid as early as possible. 524 | .SS Styles 525 | .TP 526 | \f[V]Standout, Underline, ReverseVideo, Blink, Dim, Bold, Italic, Strikethrough\f[R] 527 | Formats the text with the given style 528 | .PP 529 | If you don\[cq]t want to specify a style, leave the list empty. 530 | .SS Theme examples 531 | .IP "0." 3 532 | Set the text of \[ga]kpxhs.key\[ga] to bold 533 | .IP 534 | .nf 535 | \f[C] 536 | , ([\[dq]kpxhs\[dq],\[dq]key\[dq]], Val { fg = Def, bg = Def, styles = [Bold] } ) 537 | \f[R] 538 | .fi 539 | .IP "1." 3 540 | Set the background color of \[ga]kpxhs.key\[ga] to red 541 | .IP 542 | .nf 543 | \f[C] 544 | , ([\[dq]kpxhs\[dq],\[dq]key\[dq]], Val { fg = Def, bg = Red, styles = [] } ) 545 | \f[R] 546 | .fi 547 | .IP "2." 3 548 | Set the background color of \[ga]kpxhs.key\[ga] to red and make it bold 549 | .IP 550 | .nf 551 | \f[C] 552 | , ([\[dq]kpxhs\[dq],\[dq]key\[dq]], Val { fg = Def, bg = Red, styles = [Bold] } ) 553 | \f[R] 554 | .fi 555 | .IP "3." 3 556 | Set the background color of \[ga]kpxhs.key\[ga] to red and make it 557 | bold-italic 558 | .IP 559 | .nf 560 | \f[C] 561 | , ([\[dq]kpxhs\[dq],\[dq]key\[dq]], Val { fg = Def, bg = Red, styles = [Bold, Italic] } ) 562 | \f[R] 563 | .fi 564 | .IP "4." 3 565 | Set the background color of \[ga]kpxhs.key\[ga] to red, the foreground 566 | color to RGB(51, 187, 204) and make it bold-italic 567 | .IP 568 | .nf 569 | \f[C] 570 | , ([\[dq]kpxhs\[dq],\[dq]key\[dq]], Val { fg = RGB 51 187 204, bg = Red, styles = [Bold, Italic] } ) 571 | \f[R] 572 | .fi 573 | .SS CONFIGURATION NOTES 574 | .PP 575 | The only contents of the config and theme files is the single 576 | expression; assignments, imports, statements, and comments are not 577 | allowed. 578 | You cannot use \[ga]$\[ga] to replace parenthesis, because no arbitrary 579 | functions are evaluated. 580 | Whitespace and newline rules follow normal Haskell rules for 581 | expressions. 582 | The config and theme files are not valid Haskell modules that can be 583 | compiled; they are interpreted at launch. 584 | .PP 585 | Any records must match their specified number of fields; omission or 586 | addition of any fields will result in an invalid config, and the default 587 | will be used instead. 588 | .PP 589 | Type constructors must be written verbatim with no changes in 590 | capitalization. 591 | They include: \[ga]Just\[ga], \[ga]Nothing\[ga], \[ga]Seconds\[ga], 592 | \[ga]DoNotClear\[ga], \[ga]Val\[ga], all the color names (eg, 593 | \[ga]Red\[ga]), and all the style names (eg, \[ga]Bold\[ga]) 594 | .SH ENVIRONMENT 595 | .PP 596 | Requires keepassxc (https://github.com/keepassxreboot/keepassxc/) 597 | installed with \[ga]keepassxc-cli\[ga] in PATH. 598 | .SH FILES 599 | .TP 600 | \f[V]Configuration\f[R] 601 | \[ga]\[ti]/.config/kpxhs/config.hs\[ga] 602 | .TP 603 | \f[V]Theme\f[R] 604 | \[ga]\[ti]/.config/kpxhs/theme.hs\[ga] 605 | .SH BUGS 606 | .PP 607 | The issue tracker and repo is in: 608 | 609 | .SH LICENSE 610 | .PP 611 | GPLv3 or later 612 | .SH SEE ALSO 613 | .PP 614 | keepassxc-cli(1) 615 | -------------------------------------------------------------------------------- /docs/out/kpxhs.1.md: -------------------------------------------------------------------------------- 1 | % kpxhs(1) Version 1.11 | kpxhs manual 2 | 3 | # NAME 4 | 5 | **kpxhs** - Interactive Keepass database TUI viewer 6 | 7 | # SYNOPSIS 8 | 9 | **kpxhs** [\-h | \--help] [\-v | \--version] [\--write-config] 10 | 11 | # DESCRIPTION 12 | 13 | Interactive [Keepass](https://keepass.info/) database TUI viewer. The database and keyfile path can be configured to auto-fill on launch. Entry details can be viewed and their username and password fields can be copied to clipboard. On exit, if the clipboard is "dirty" then it will offer to clear it. The clipboard can also be cleared after a configurable timeout, along with a progress bar that counts down to clear. 14 | 15 | # OPTIONS 16 | 17 | `h, help` 18 | 19 | : Prints the help message and exit. Ignores number of dashes. 20 | 21 | `v, version` 22 | 23 | : Prints the version number and exit. Ignores number of dashes. 24 | 25 | `write-config` 26 | 27 | : Writes the default config.hs and theme.hs files to ~/.config/kpxhs/. Ignores number of dashes. 28 | 29 | # KEYBINDINGS 30 | 31 | `q` 32 | : Go up a directory, or attempt to quit if in root directory. 33 | 34 | `Esc` 35 | : Focus or go back to browser (from entry details or search) 36 | 37 | `Tab, Shift-Tab` 38 | : Cycle focus between elements in login fields and exit dialog 39 | 40 | `Enter` 41 | : Show entry details of selected entry. Attempts login if locked 42 | 43 | `u` 44 | : Copy username of selected/current entry 45 | 46 | `p` 47 | : Copy password of selected/current entry 48 | 49 | `[n]j, [n]s` 50 | : Move down the list by \`n\` items, default is 1. \`n\` is an optional series of digits (0-9), that terminates when \`j\` or \`s\` is pressed. This resembles the vim motion commands. Leading zeros are stripped. For example, \`4j\` will move four items down and \`10j\` will move ten items down. Use the relative line numbers to help; the number shown is the exact number needed for the digit \`n\` 51 | 52 | `[n]k, [n]w` 53 | : Move up the list by \`n\` items, default is 1. \`n\` is an optional series of digits (0-9), that terminates when \`k\` or \`w\` is pressed. Leading zeros are stripped. This resembles the vim motion commands. For example, \`4k\` will move four items down and \`10k\` will move ten items down. Use the relative line numbers to help; the number shown is the exact number needed for the digit \`n\` 54 | 55 | `g` 56 | : Move to the top of the list 57 | 58 | `G` 59 | : Move to the bottom of the list 60 | 61 | The following table shows a summary of the keybindings and their effects in each mode 62 | 63 | |Key | Browser | Search | Entry details| Login | Exit dialog 64 | |-----|-------------------|--------------|--------------|------------|------------ 65 | |q | Go up dir or quit | - | - | - | - 66 | |Esc | Clear command | Focus Browser| Back | Quit | - 67 | |/ | Focus Search | - | - | - | - 68 | |Tab | - | - | - | Cycle Focus| Cycle Focus 69 | |Enter| Show details | - | - | Unlock | - 70 | |j | Move down | - | - | - | - 71 | |k | Move up | - | - | - | - 72 | |u | Copy username | - | - | - | - 73 | |p | Copy password | - | - | - | - 74 | |g | Go to top | - | - | - | - 75 | |G | Go to bottom | - | - | - | - 76 | 77 | 78 | # EXAMPLE USAGE 79 | 80 | 1. \`kpxhs\` 81 | 2. \`YOUR_PASSWORD\` (Assuming database path stored in config and no keyfile) 82 | 3. \`/\` (Focus search bar) 83 | 4. \`git\` (Filter list to items with "git" in title) 84 | 5. \`Esc\` (Focus to list) 85 | 6. \`j\` (Focus one entry below) 86 | 7. \`p\` (Copy password) 87 | 8. \`Esc\` (quit) 88 | 9. (Focus is on clear clipboard and exit by default) \`Enter\` (clear clipboard and exit) 89 | 90 | # CONFIGURATION 91 | 92 | ## INTRODUCTION 93 | 94 | You can set the database and keyfile fields to be auto-filled with any path, so you only need to enter your password on launch. You can also customize the automatic clipboard clearing: change the number of seconds to wait before clearing the clipboard; or disable the feature altogether. 95 | 96 | Hint: run \`kpxhs --write-config\` to generate the default config and theme for further editing 97 | 98 | ## SETTINGS 99 | 100 | The config file is located in \`~/.config/kpxhs/config.hs\`. Make sure it is encoded as UTF-8. Write something like: 101 | 102 | ```hs 103 | Config { timeout = Just (Seconds 10) 104 | , dbPath = Just "/home/me/kpxhs/test/kpxhs_test.kdbx" 105 | , keyfilePath = Just "/home/me/kpxhs/test/keyfile.key" 106 | , focusSearchOnStart = Just False 107 | } 108 | ``` 109 | 110 | **It must be a valid Haskell expression** of a record with four fields: timeout, dbPath, keyfilePath, and focusSearchOnStart. All three are Maybe types - they are optional and you can always omit specifying them by writing \`Nothing\`. Do not delete a field however, as it will result in an invalid config. 111 | 112 | The paths can be any UTF-8 string; no validation is performed on them. 113 | 114 | ### timeout 115 | 116 | After copying a username or password, *kpxhs* can automatically clear the clipboard after a set number of seconds. There are three valid values for the timeout field: 117 | 118 | `Just (Seconds t)` 119 | : Set the number of seconds to wait for \`t\` seconds after the copy for clearing the clipboard. The number of seconds must be an integer. 120 | 121 | `Just DoNotClear` 122 | : Disable automatic clipboard clearing. 123 | 124 | `Nothing` 125 | : Fall back to the default, which is **10 seconds** 126 | 127 | ### dbPath 128 | 129 | *kpxhs* can auto-fill the database path with a given string. There are two valid values: 130 | 131 | `Just "xxx"` 132 | : Fill in the database path with the string "xxx", without quotes. 133 | 134 | `Nothing` 135 | : Fall back to the default, which is the empty string "" 136 | 137 | ### keyfilePath 138 | 139 | *kpxhs* can auto-fill the keyfile path with a given string. There are two valid values: 140 | 141 | `Just "xxx"` 142 | : Fill in the keyfile path with the string "xxx", without quotes. 143 | 144 | `Nothing` 145 | : Fall back to the default, which is the empty string "" 146 | 147 | ### focusSearchOnStart 148 | 149 | Whether to focus the search bar after initial login, so `/` key doesn't have to be pressed to focus it. There are two valid values: 150 | 151 | `Just True` 152 | : Focus the search bar after initial login 153 | 154 | `Just False` 155 | : Focus the browser list after initial login 156 | 157 | `Nothing` 158 | : Fall back to the default, which is to focus the browser list after initial login 159 | 160 | 161 | ## THEMING 162 | 163 | The theme file is located in \`~/.config/kpxhs/theme.hs\`. Make sure it is encoded as UTF-8. You should probably edit the default theme instead of writing from scratch, because if you write from scratch, all the colors in the default theme are lost. 164 | 165 | This is the default theme if you don't provide any: 166 | 167 | ```hs 168 | [ (["edit"], Val { fg = Black, bg = White, styles = [] }) 169 | , (["edit","focused"], Val { fg = White, bg = Blue, styles = [] }) 170 | , (["dialog"], Val { fg = White, bg = Blue, styles = [] }) 171 | , (["button"], Val { fg = Black, bg = White, styles = [] }) 172 | , (["button","selected"], Val { fg = Def, bg = Yellow, styles = [] }) 173 | , (["progressComplete"], Val { fg = White, bg = Blue, styles = [] }) 174 | , (["kpxhs","key"], Val { fg = Def, bg = White, styles = [] }) 175 | , (["kpxhs","label"], Val { fg = Black, bg = Def, styles = [] }) 176 | , (["kpxhs","line_number"], Val { fg = Yellow, bg = Def, styles = [] }) 177 | , (["kpxhs","line_number","focused"], Val { fg = Red, bg = Def, styles = [Bold]}) 178 | , (["kpxhs","list_border"], Val { fg = Black, bg = Def, styles = [] }) 179 | , (["kpxhs","list_border","focused"], Val { fg = Blue, bg = Def, styles = [] }) 180 | , (["kpxhs","directory"], Val { fg = Black, bg = Def, styles = [Bold]}) 181 | , (["kpxhs","directory","focused"], Val { fg = Red, bg = Def, styles = [Bold]}) 182 | , (["kpxhs","go_up"], Val { fg = Green 183 | , bg = Def 184 | , styles = [Bold, Italic] 185 | }) 186 | , (["kpxhs","go_up","focused"], Val { fg = Blue 187 | , bg = Def 188 | , styles = [Bold, Italic] 189 | }) 190 | , (["kpxhs","entry"], Val { fg = Black, bg = Def, styles = [] }) 191 | , (["kpxhs","entry","focused"], Val { fg = Red, bg = Def, styles = [] }) 192 | ] 193 | ``` 194 | 195 | **The theme file must be a valid Haskell expression**. It is a list-of-2-tuples; for every tuple, the first item is a list-of-strings representing the attribute name, and the second item is the attribute value. The attribute value is represented as a record with three fields: fg, bg, and styles. The fg and bg fields only accept certain color names. styles is a list-of-styles, and also only accept certain style names. 196 | 197 | ### Attribute names 198 | 199 | `["xxx", "yyy"]` 200 | : Represents an attribute name "xxx.yyy". Must have at least one item. 201 | 202 | There are a few special attribute names exclusive to *kpxhs*. They are appropriately namespaced with \`"kpxhs"\`. 203 | 204 | `["kpxhs", "key"]` 205 | : The key being bound (eg, "Esc") 206 | 207 | `["kpxhs", "label"]` 208 | : The label bound (eg, "exit") 209 | 210 | In other words, the footer shows a nano-like grid of keys and their action. For example, "Esc exit" to indicate that pressing the Esc key will exit. \`kpxhs.key\` would style the "Esc" text and \`kpxhs.label\` would style the "exit" text 211 | 212 | `["kpxhs", "line_number"]` 213 | : The relative line numbers on the left side of the list, for entries that are not selected/in focus 214 | 215 | `["kpxhs", "line_number", "focused"]` 216 | : The relative line numbers on the left side of the list for the currently selected entry 217 | 218 | `["kpxhs", "list_border"]` 219 | : The list/browser border when it is not focused (ie, focus is on search bar). Only foreground color is used. 220 | 221 | `["kpxhs", "list_border", "focused"]` 222 | : The list/browser border when it is focused (ie, focus is on list). Only foreground color is used. 223 | 224 | `["kpxhs", "directory"]` 225 | : A directory that is not currently selected 226 | 227 | `["kpxhs", "directory", "focused"]` 228 | : A directory that is currently selected 229 | 230 | `["kpxhs", "go_up"]` 231 | : The "\-- (Go up directory) \--" text when it is not focused/selected 232 | 233 | `["kpxhs", "go_up", "focused"]` 234 | : The "\-- (Go up directory) \--" text when it is focused/selected 235 | 236 | `["kpxhs", "entry"]` 237 | : An entry that is not currently selected 238 | 239 | `["kpxhs", "entry", "focused"]` 240 | : An entry that is currently selected 241 | 242 | Apart from those, you can use any other attribute name of elements used in the program. Here are the Brick docs for the attribute names of the elements used in *kpxhs*: 243 | 244 | - [List widget](https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-List.html#g:7) 245 | - [Exit dialog](https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-Dialog.html#g:4) 246 | - [Login dialog](https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-Edit.html#g:7) 247 | - [Progress bar](https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-ProgressBar.html#g:1) 248 | - [Borders](https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-Border.html#g:5) 249 | 250 | ### Attribute values 251 | 252 | The record has three fields: 253 | 254 | `fg` 255 | : Set the foreground color. See **Colors** 256 | 257 | `bg` 258 | : Set the background color. See **Colors** 259 | 260 | `styles` 261 | : Set the given styles. See **Styles** 262 | 263 | ### Colors 264 | 265 | 266 | `Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, BrightBlack, BrightRed, BrightGreen, BrightYellow, BrightBlue, BrightMagenta, BrightCyan, BrightWhite` 267 | : Set the color to one of those 16 colors. Their exact values are configured through your terminal 268 | 269 | `Def` 270 | : Use the default color for that element. Essentially means a color is not specified 271 | 272 | `RGB r g b` 273 | : Use an RGB color given by the three integers, from 0 to 255 inclusive. Note that Brick doesn't support the entire rgb palette, so some colors can throw an error. *kpxhs* allows it to be thrown, because some attributes might be a hassle to navigate to, so aborting the program will let the user know their color is invalid as early as possible. 274 | 275 | ### Styles 276 | 277 | `Standout, Underline, ReverseVideo, Blink, Dim, Bold, Italic, Strikethrough` 278 | : Formats the text with the given style 279 | 280 | If you don't want to specify a style, leave the list empty. 281 | 282 | ### Theme examples 283 | 284 | 0. Set the text of \`kpxhs.key\` to bold 285 | ```hs 286 | , (["kpxhs","key"], Val { fg = Def, bg = Def, styles = [Bold] } ) 287 | ``` 288 | 289 | 1. Set the background color of \`kpxhs.key\` to red 290 | ```hs 291 | , (["kpxhs","key"], Val { fg = Def, bg = Red, styles = [] } ) 292 | ``` 293 | 294 | 2. Set the background color of \`kpxhs.key\` to red and make it bold 295 | 296 | ```hs 297 | , (["kpxhs","key"], Val { fg = Def, bg = Red, styles = [Bold] } ) 298 | ``` 299 | 300 | 3. Set the background color of \`kpxhs.key\` to red and make it bold-italic 301 | 302 | ```hs 303 | , (["kpxhs","key"], Val { fg = Def, bg = Red, styles = [Bold, Italic] } ) 304 | ``` 305 | 306 | 4. Set the background color of \`kpxhs.key\` to red, the foreground color to RGB(51, 187, 204) and make it bold-italic 307 | 308 | ```hs 309 | , (["kpxhs","key"], Val { fg = RGB 51 187 204, bg = Red, styles = [Bold, Italic] } ) 310 | ``` 311 | 312 | ## CONFIGURATION NOTES 313 | 314 | The only contents of the config and theme files is the single expression; assignments, imports, statements, and comments are not allowed. You cannot use \`$\` to replace parenthesis, because no arbitrary functions are evaluated. Whitespace and newline rules follow normal Haskell rules for expressions. The config and theme files are not valid Haskell modules that can be compiled; they are interpreted at launch. 315 | 316 | Any records must match their specified number of fields; omission or addition of any fields will result in an invalid config, and the default will be used instead. 317 | 318 | Type constructors must be written verbatim with no changes in capitalization. They include: \`Just\`, \`Nothing\`, \`Seconds\`, \`DoNotClear\`, \`Val\`, all the color names (eg, \`Red\`), and all the style names (eg, \`Bold\`) 319 | 320 | 321 | # ENVIRONMENT 322 | 323 | Requires [keepassxc](https://github.com/keepassxreboot/keepassxc/) installed with \`keepassxc-cli\` in PATH. 324 | 325 | # FILES 326 | 327 | `Configuration` 328 | 329 | : \`~/.config/kpxhs/config.hs\` 330 | 331 | 332 | `Theme` 333 | 334 | : \`~/.config/kpxhs/theme.hs\` 335 | 336 | # BUGS 337 | 338 | The issue tracker and repo is in: 339 | 340 | # LICENSE 341 | 342 | GPLv3 or later 343 | 344 | # SEE ALSO 345 | 346 | keepassxc-cli(1) 347 | -------------------------------------------------------------------------------- /docs/src/part-1.md: -------------------------------------------------------------------------------- 1 | % kpxhs(1) Version 1.11 | kpxhs manual 2 | 3 | # NAME 4 | 5 | **kpxhs** - Interactive Keepass database TUI viewer 6 | 7 | # SYNOPSIS 8 | 9 | **kpxhs** [\-h | \--help] [\-v | \--version] [\--write-config] 10 | 11 | # DESCRIPTION 12 | 13 | Interactive [Keepass](https://keepass.info/) database TUI viewer. The database and keyfile path can be configured to auto-fill on launch. Entry details can be viewed and their username and password fields can be copied to clipboard. On exit, if the clipboard is "dirty" then it will offer to clear it. The clipboard can also be cleared after a configurable timeout, along with a progress bar that counts down to clear. 14 | 15 | # OPTIONS 16 | 17 | `h, help` 18 | 19 | : Prints the help message and exit. Ignores number of dashes. 20 | 21 | `v, version` 22 | 23 | : Prints the version number and exit. Ignores number of dashes. 24 | 25 | `write-config` 26 | 27 | : Writes the default config.hs and theme.hs files to ~/.config/kpxhs/. Ignores number of dashes. 28 | 29 | # KEYBINDINGS 30 | 31 | `q` 32 | : Go up a directory, or attempt to quit if in root directory. 33 | 34 | `Esc` 35 | : Focus or go back to browser (from entry details or search) 36 | 37 | `Tab, Shift-Tab` 38 | : Cycle focus between elements in login fields and exit dialog 39 | 40 | `Enter` 41 | : Show entry details of selected entry. Attempts login if locked 42 | 43 | `u` 44 | : Copy username of selected/current entry 45 | 46 | `p` 47 | : Copy password of selected/current entry 48 | 49 | `[n]j, [n]s` 50 | : Move down the list by \`n\` items, default is 1. \`n\` is an optional series of digits (0-9), that terminates when \`j\` or \`s\` is pressed. This resembles the vim motion commands. Leading zeros are stripped. For example, \`4j\` will move four items down and \`10j\` will move ten items down. Use the relative line numbers to help; the number shown is the exact number needed for the digit \`n\` 51 | 52 | `[n]k, [n]w` 53 | : Move up the list by \`n\` items, default is 1. \`n\` is an optional series of digits (0-9), that terminates when \`k\` or \`w\` is pressed. Leading zeros are stripped. This resembles the vim motion commands. For example, \`4k\` will move four items down and \`10k\` will move ten items down. Use the relative line numbers to help; the number shown is the exact number needed for the digit \`n\` 54 | 55 | `g` 56 | : Move to the top of the list 57 | 58 | `G` 59 | : Move to the bottom of the list 60 | 61 | The following table shows a summary of the keybindings and their effects in each mode 62 | 63 | |Key | Browser | Search | Entry details| Login | Exit dialog 64 | |-----|-------------------|--------------|--------------|------------|------------ 65 | |q | Go up dir or quit | - | - | - | - 66 | |Esc | Clear command | Focus Browser| Back | Quit | - 67 | |/ | Focus Search | - | - | - | - 68 | |Tab | - | - | - | Cycle Focus| Cycle Focus 69 | |Enter| Show details | - | - | Unlock | - 70 | |j | Move down | - | - | - | - 71 | |k | Move up | - | - | - | - 72 | |u | Copy username | - | - | - | - 73 | |p | Copy password | - | - | - | - 74 | |g | Go to top | - | - | - | - 75 | |G | Go to bottom | - | - | - | - 76 | 77 | 78 | # EXAMPLE USAGE 79 | 80 | 1. \`kpxhs\` 81 | 2. \`YOUR_PASSWORD\` (Assuming database path stored in config and no keyfile) 82 | 3. \`/\` (Focus search bar) 83 | 4. \`git\` (Filter list to items with "git" in title) 84 | 5. \`Esc\` (Focus to list) 85 | 6. \`j\` (Focus one entry below) 86 | 7. \`p\` (Copy password) 87 | 8. \`Esc\` (quit) 88 | 9. (Focus is on clear clipboard and exit by default) \`Enter\` (clear clipboard and exit) 89 | 90 | # CONFIGURATION 91 | 92 | ## INTRODUCTION 93 | 94 | You can set the database and keyfile fields to be auto-filled with any path, so you only need to enter your password on launch. You can also customize the automatic clipboard clearing: change the number of seconds to wait before clearing the clipboard; or disable the feature altogether. 95 | 96 | Hint: run \`kpxhs --write-config\` to generate the default config and theme for further editing 97 | 98 | ## SETTINGS 99 | 100 | The config file is located in \`~/.config/kpxhs/config.hs\`. Make sure it is encoded as UTF-8. Write something like: 101 | 102 | ```hs 103 | Config { timeout = Just (Seconds 10) 104 | , dbPath = Just "/home/me/kpxhs/test/kpxhs_test.kdbx" 105 | , keyfilePath = Just "/home/me/kpxhs/test/keyfile.key" 106 | , focusSearchOnStart = Just False 107 | } 108 | ``` 109 | 110 | **It must be a valid Haskell expression** of a record with four fields: timeout, dbPath, keyfilePath, and focusSearchOnStart. All three are Maybe types - they are optional and you can always omit specifying them by writing \`Nothing\`. Do not delete a field however, as it will result in an invalid config. 111 | 112 | The paths can be any UTF-8 string; no validation is performed on them. 113 | 114 | ### timeout 115 | 116 | After copying a username or password, *kpxhs* can automatically clear the clipboard after a set number of seconds. There are three valid values for the timeout field: 117 | 118 | `Just (Seconds t)` 119 | : Set the number of seconds to wait for \`t\` seconds after the copy for clearing the clipboard. The number of seconds must be an integer. 120 | 121 | `Just DoNotClear` 122 | : Disable automatic clipboard clearing. 123 | 124 | `Nothing` 125 | : Fall back to the default, which is **10 seconds** 126 | 127 | ### dbPath 128 | 129 | *kpxhs* can auto-fill the database path with a given string. There are two valid values: 130 | 131 | `Just "xxx"` 132 | : Fill in the database path with the string "xxx", without quotes. If non-empty, the default focus when starting the app is on the password field. 133 | 134 | `Nothing` 135 | : Fall back to the default, which is the empty string "". The default focus when starting the app is on the database path field. 136 | 137 | ### keyfilePath 138 | 139 | *kpxhs* can auto-fill the keyfile path with a given string. There are two valid values: 140 | 141 | `Just "xxx"` 142 | : Fill in the keyfile path with the string "xxx", without quotes. 143 | 144 | `Nothing` 145 | : Fall back to the default, which is the empty string "" 146 | 147 | ### focusSearchOnStart 148 | 149 | Whether to focus the search bar after initial login, so `/` key doesn't have to be pressed to focus it. There are two valid values: 150 | 151 | `Just True` 152 | : Focus the search bar after initial login 153 | 154 | `Just False` 155 | : Focus the browser list after initial login 156 | 157 | `Nothing` 158 | : Fall back to the default, which is to focus the browser list after initial login 159 | 160 | 161 | ## THEMING 162 | 163 | The theme file is located in \`~/.config/kpxhs/theme.hs\`. Make sure it is encoded as UTF-8. You should probably edit the default theme instead of writing from scratch, because if you write from scratch, all the colors in the default theme are lost. 164 | 165 | This is the default theme if you don't provide any: 166 | 167 | ```hs 168 | -------------------------------------------------------------------------------- /docs/src/part-2.md: -------------------------------------------------------------------------------- 1 | ``` 2 | 3 | **The theme file must be a valid Haskell expression**. It is a list-of-2-tuples; for every tuple, the first item is a list-of-strings representing the attribute name, and the second item is the attribute value. The attribute value is represented as a record with three fields: fg, bg, and styles. The fg and bg fields only accept certain color names. styles is a list-of-styles, and also only accept certain style names. 4 | 5 | ### Attribute names 6 | 7 | `["xxx", "yyy"]` 8 | : Represents an attribute name "xxx.yyy". Must have at least one item. 9 | 10 | There are a few special attribute names exclusive to *kpxhs*. They are appropriately namespaced with \`"kpxhs"\`. 11 | 12 | `["kpxhs", "key"]` 13 | : The key being bound (eg, "Esc") 14 | 15 | `["kpxhs", "label"]` 16 | : The label bound (eg, "exit") 17 | 18 | In other words, the footer shows a nano-like grid of keys and their action. For example, "Esc exit" to indicate that pressing the Esc key will exit. \`kpxhs.key\` would style the "Esc" text and \`kpxhs.label\` would style the "exit" text 19 | 20 | `["kpxhs", "line_number"]` 21 | : The relative line numbers on the left side of the list, for entries that are not selected/in focus 22 | 23 | `["kpxhs", "line_number", "focused"]` 24 | : The relative line numbers on the left side of the list for the currently selected entry 25 | 26 | `["kpxhs", "list_border"]` 27 | : The list/browser border when it is not focused (ie, focus is on search bar). Only foreground color is used. 28 | 29 | `["kpxhs", "list_border", "focused"]` 30 | : The list/browser border when it is focused (ie, focus is on list). Only foreground color is used. 31 | 32 | `["kpxhs", "directory"]` 33 | : A directory that is not currently selected 34 | 35 | `["kpxhs", "directory", "focused"]` 36 | : A directory that is currently selected 37 | 38 | `["kpxhs", "go_up"]` 39 | : The "\-- (Go up directory) \--" text when it is not focused/selected 40 | 41 | `["kpxhs", "go_up", "focused"]` 42 | : The "\-- (Go up directory) \--" text when it is focused/selected 43 | 44 | `["kpxhs", "entry"]` 45 | : An entry that is not currently selected 46 | 47 | `["kpxhs", "entry", "focused"]` 48 | : An entry that is currently selected 49 | 50 | Apart from those, you can use any other attribute name of elements used in the program. Here are the Brick docs for the attribute names of the elements used in *kpxhs*: 51 | 52 | - [List widget](https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-List.html#g:7) 53 | - [Exit dialog](https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-Dialog.html#g:4) 54 | - [Login dialog](https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-Edit.html#g:7) 55 | - [Progress bar](https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-ProgressBar.html#g:1) 56 | - [Borders](https://hackage.haskell.org/package/brick-0.64/docs/Brick-Widgets-Border.html#g:5) 57 | 58 | ### Attribute values 59 | 60 | The record has three fields: 61 | 62 | `fg` 63 | : Set the foreground color. See **Colors** 64 | 65 | `bg` 66 | : Set the background color. See **Colors** 67 | 68 | `styles` 69 | : Set the given styles. See **Styles** 70 | 71 | ### Colors 72 | 73 | 74 | `Black, Red, Green, Yellow, Blue, Magenta, Cyan, White, BrightBlack, BrightRed, BrightGreen, BrightYellow, BrightBlue, BrightMagenta, BrightCyan, BrightWhite` 75 | : Set the color to one of those 16 colors. Their exact values are configured through your terminal 76 | 77 | `Def` 78 | : Use the default color for that element. Essentially means a color is not specified 79 | 80 | `RGB r g b` 81 | : Use an RGB color given by the three integers, from 0 to 255 inclusive. Note that Brick doesn't support the entire rgb palette, so some colors can throw an error. *kpxhs* allows it to be thrown, because some attributes might be a hassle to navigate to, so aborting the program will let the user know their color is invalid as early as possible. 82 | 83 | ### Styles 84 | 85 | `Standout, Underline, ReverseVideo, Blink, Dim, Bold, Italic, Strikethrough` 86 | : Formats the text with the given style 87 | 88 | If you don't want to specify a style, leave the list empty. 89 | 90 | ### Theme examples 91 | 92 | 0. Set the text of \`kpxhs.key\` to bold 93 | ```hs 94 | , (["kpxhs","key"], Val { fg = Def, bg = Def, styles = [Bold] } ) 95 | ``` 96 | 97 | 1. Set the background color of \`kpxhs.key\` to red 98 | ```hs 99 | , (["kpxhs","key"], Val { fg = Def, bg = Red, styles = [] } ) 100 | ``` 101 | 102 | 2. Set the background color of \`kpxhs.key\` to red and make it bold 103 | 104 | ```hs 105 | , (["kpxhs","key"], Val { fg = Def, bg = Red, styles = [Bold] } ) 106 | ``` 107 | 108 | 3. Set the background color of \`kpxhs.key\` to red and make it bold-italic 109 | 110 | ```hs 111 | , (["kpxhs","key"], Val { fg = Def, bg = Red, styles = [Bold, Italic] } ) 112 | ``` 113 | 114 | 4. Set the background color of \`kpxhs.key\` to red, the foreground color to RGB(51, 187, 204) and make it bold-italic 115 | 116 | ```hs 117 | , (["kpxhs","key"], Val { fg = RGB 51 187 204, bg = Red, styles = [Bold, Italic] } ) 118 | ``` 119 | 120 | ## CONFIGURATION NOTES 121 | 122 | The only contents of the config and theme files is the single expression; assignments, imports, statements, and comments are not allowed. You cannot use \`$\` to replace parenthesis, because no arbitrary functions are evaluated. Whitespace and newline rules follow normal Haskell rules for expressions. The config and theme files are not valid Haskell modules that can be compiled; they are interpreted at launch. 123 | 124 | Any records must match their specified number of fields; omission or addition of any fields will result in an invalid config, and the default will be used instead. 125 | 126 | Type constructors must be written verbatim with no changes in capitalization. They include: \`Just\`, \`Nothing\`, \`Seconds\`, \`DoNotClear\`, \`Val\`, all the color names (eg, \`Red\`), and all the style names (eg, \`Bold\`) 127 | 128 | 129 | # ENVIRONMENT 130 | 131 | Requires [keepassxc](https://github.com/keepassxreboot/keepassxc/) installed with \`keepassxc-cli\` in PATH. 132 | 133 | # FILES 134 | 135 | `Configuration` 136 | 137 | : \`~/.config/kpxhs/config.hs\` 138 | 139 | 140 | `Theme` 141 | 142 | : \`~/.config/kpxhs/theme.hs\` 143 | 144 | # BUGS 145 | 146 | The issue tracker and repo is in: 147 | 148 | # LICENSE 149 | 150 | GPLv3 or later 151 | 152 | # SEE ALSO 153 | 154 | keepassxc-cli(1) 155 | -------------------------------------------------------------------------------- /docs/src/section.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | STARTING_COMMENT = '-- EMBED START' 4 | ENDING_COMMENT = '-- EMBED END' 5 | 6 | with open('../src/kpxhs/Config/Defaults.hs', 'r') as f: 7 | f = f.readlines() 8 | 9 | for idx, line in enumerate(f): 10 | if line == f'{STARTING_COMMENT}\n': 11 | break 12 | 13 | # Skip the comment, the type signature, and the function assignment 14 | offset = idx + 3 15 | 16 | section = [] 17 | for line in f[offset:]: 18 | if line != f'{ENDING_COMMENT}\n': 19 | section.append(line[2:]) 20 | else: 21 | break 22 | 23 | Path('out').mkdir(exist_ok=True) 24 | Path('out/default_theme.hs').touch(exist_ok=True) 25 | 26 | with open('out/default_theme.hs', 'w+') as f: 27 | f.write(''.join(section)) 28 | -------------------------------------------------------------------------------- /kpxhs.cabal: -------------------------------------------------------------------------------- 1 | name: kpxhs 2 | version: 1.11 3 | synopsis: Interactive Keepass database TUI viewer 4 | description: Supports copying username and password; based on keepassxc-cli 5 | homepage: https://github.com/akazukin5151/kpxhs 6 | license: GPL-3 7 | license-file: LICENSE 8 | author: akazukin5151 9 | maintainer: tsuiyikching@protonmail.com 10 | copyright: 2021 akazukin5151 11 | category: Password 12 | build-type: Simple 13 | cabal-version: >=1.10 14 | extra-source-files: README.md 15 | 16 | executable kpxhs 17 | hs-source-dirs: src/kpxhs 18 | main-is: Main.hs 19 | other-modules: Types, 20 | Common, 21 | Config.Config, 22 | Config.Eval, 23 | Config.Defaults, 24 | Config.Types, 25 | Constants, 26 | UI, 27 | UI.Common, 28 | UI.BrowserUI, 29 | UI.ExitDialogUI, 30 | UI.LoginUI, 31 | UI.EntryDetailsUI, 32 | Events, 33 | ViewEvents.BrowserEvents.BrowserEvents, 34 | ViewEvents.BrowserEvents.Core, 35 | ViewEvents.BrowserEvents.Utils, 36 | ViewEvents.BrowserEvents.Fork, 37 | ViewEvents.BrowserEvents.Event, 38 | ViewEvents.BrowserEvents.VimCommand, 39 | ViewEvents.Common, 40 | ViewEvents.SearchEvents, 41 | ViewEvents.EntryDetailsEvents, 42 | ViewEvents.LoginEvents, 43 | ViewEvents.LoginFrozenEvents, 44 | ViewEvents.ExitDialogEvents, 45 | ViewEvents.Copy, 46 | ViewEvents.Utils 47 | 48 | default-language: Haskell2010 49 | ghc-options: -threaded 50 | -Weverything 51 | -Wno-missing-exported-signatures 52 | -Wno-missing-import-lists 53 | -Wno-missed-specialisations 54 | -Wno-all-missed-specialisations 55 | -Wno-unsafe 56 | -Wno-safe 57 | -Wno-missing-safe-haskell-mode 58 | -Wno-missing-local-signatures 59 | -Wno-monomorphism-restriction 60 | -Wno-implicit-prelude 61 | -Wno-prepositive-qualified-module 62 | -Wno-missing-deriving-strategies 63 | build-depends: base >= 4.7 && < 5, 64 | brick >= 0.62 && < 0.68, 65 | vty >= 5.33 && < 5.40, 66 | microlens-th >= 0.4.3.10 && < 0.5, 67 | microlens >= 0.4.12.0 && < 0.5, 68 | vector >= 0.12 && < 0.13 , 69 | text >= 1.2.4.1 && < 1.3, 70 | process >= 1.6.13.2 && < 1.7, 71 | text-zipper >= 0.11 && < 0.20, 72 | directory >= 1.3.6.0 && < 1.4, 73 | containers >= 0.6 && < 0.7, 74 | bytestring >= 0.10.12.0 && < 0.11, 75 | filepath >= 1.4.2.1 && < 1.5, 76 | file-embed >= 0.0.15 && < 0.1.0 77 | -------------------------------------------------------------------------------- /pics/browser1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akazukin5151/kpxhs/683f71ae8fa5a77c47f38df68133b1cf251b8238/pics/browser1.png -------------------------------------------------------------------------------- /pics/browser2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akazukin5151/kpxhs/683f71ae8fa5a77c47f38df68133b1cf251b8238/pics/browser2.png -------------------------------------------------------------------------------- /pics/clear_clip.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akazukin5151/kpxhs/683f71ae8fa5a77c47f38df68133b1cf251b8238/pics/clear_clip.png -------------------------------------------------------------------------------- /pics/countdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akazukin5151/kpxhs/683f71ae8fa5a77c47f38df68133b1cf251b8238/pics/countdown.png -------------------------------------------------------------------------------- /pics/entry.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akazukin5151/kpxhs/683f71ae8fa5a77c47f38df68133b1cf251b8238/pics/entry.png -------------------------------------------------------------------------------- /pics/responsive_footer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akazukin5151/kpxhs/683f71ae8fa5a77c47f38df68133b1cf251b8238/pics/responsive_footer.png -------------------------------------------------------------------------------- /pics/searching.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akazukin5151/kpxhs/683f71ae8fa5a77c47f38df68133b1cf251b8238/pics/searching.png -------------------------------------------------------------------------------- /pics/unlock.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akazukin5151/kpxhs/683f71ae8fa5a77c47f38df68133b1cf251b8238/pics/unlock.png -------------------------------------------------------------------------------- /src/kpxhs/Common.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Common where 4 | 5 | import Brick.AttrMap (AttrName) 6 | import qualified Brick.Focus as F 7 | import Brick.Markup (Markup, markup, (@?)) 8 | import Brick.Types (Widget) 9 | import qualified Brick.Widgets.Dialog as D 10 | import qualified Brick.Widgets.List as L 11 | import Data.Map.Strict ((!?)) 12 | import Data.Text (Text) 13 | import qualified Data.Text as TT 14 | import qualified Data.Vector as Vec 15 | import Lens.Micro ((^.)) 16 | 17 | import Types 18 | ( ExitDialog (Cancel, Clear, Exit) 19 | , Field (BrowserField, PasswordField, PathField) 20 | , State 21 | , allEntryDetails 22 | , currentPath 23 | , selectedEntryName 24 | ) 25 | 26 | -- | This should only be used for running the show cmd 27 | pathToStr :: [Text] -> Text 28 | pathToStr = TT.concat 29 | 30 | -- | This should be used for accessing any other mappings in the state 31 | pathToStrRoot :: [Text] -> Text 32 | pathToStrRoot x = 33 | case pathToStr x of 34 | "" -> "." 35 | y -> y 36 | 37 | annotate :: [(Text, Text)] -> Widget Field 38 | annotate x = markup $ foldr1 (<>) (f <$> x) 39 | where 40 | -- The mappend is on AttrName, not on String 41 | f :: (Text, Text) -> Markup AttrName 42 | f (key, label) = (key @? ("kpxhs" <> "key")) <> (label @? ("kpxhs" <> "label")) 43 | 44 | exit :: (Text, Text) 45 | exit = ("Esc", " exit ") 46 | 47 | tab :: Text -> (Text, Text) 48 | tab label = ("Tab", label) 49 | 50 | initialFooter :: F.FocusRing Field -> [(Text, Text)] 51 | initialFooter fr = 52 | case F.focusGetCurrent fr of 53 | Just PathField -> exit_tab_submit "password" 54 | Just PasswordField -> exit_tab_submit "keyfile" 55 | _ -> exit_tab_submit "path" 56 | where 57 | exit_tab_submit x = 58 | [exit, tab (" focus " <> x <> " field "), ("Enter", " submit")] 59 | 60 | toBrowserList :: [Text] -> L.List Field Text 61 | toBrowserList xs = L.list BrowserField (Vec.fromList xs) 1 62 | 63 | maybeGetEntryData :: State -> Maybe Text 64 | maybeGetEntryData st = do 65 | let dirname = pathToStrRoot (st^.currentPath) 66 | entryname <- st^.selectedEntryName 67 | entriesInThisDir <- (st^.allEntryDetails) !? dirname 68 | entriesInThisDir !? entryname 69 | 70 | defaultDialog :: D.Dialog ExitDialog 71 | defaultDialog = D.dialog Nothing (Just (0, defaultDialogChoices)) 60 72 | 73 | defaultDialogChoices :: [(String, ExitDialog)] 74 | defaultDialogChoices = 75 | [ ("Clear and exit", Clear) 76 | , ("Just exit", Exit) 77 | , ("Do not exit", Cancel) 78 | ] 79 | -------------------------------------------------------------------------------- /src/kpxhs/Config/Config.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Config.Config (parseConfig) where 4 | 5 | import qualified Brick.Focus as F 6 | import Control.Exception (IOException) 7 | import Control.Exception.Base (catch) 8 | import Data.Bifunctor (Bifunctor (bimap)) 9 | import qualified Data.ByteString as B 10 | import Data.Maybe (fromMaybe) 11 | import Data.Text (Text, unpack) 12 | import Data.Text.Encoding (decodeUtf8') 13 | import System.FilePath (()) 14 | import Text.Read (readMaybe) 15 | 16 | import Config.Defaults (defaultConfig, defaultTheme) 17 | import Config.Eval (eval, evalName) 18 | import Config.Types 19 | ( ActualTheme 20 | , Config (dbPath, keyfilePath, timeout, focusSearchOnStart) 21 | , Timeout (DoNotClear, Seconds) 22 | ) 23 | import Types (Field (KeyfileField, PasswordField, PathField, SearchField, BrowserField)) 24 | 25 | 26 | fallback :: IOException -> IO B.ByteString 27 | fallback _ = pure "" 28 | 29 | parseConfig :: String -> IO (Maybe Int, Text, Text, F.FocusRing Field, ActualTheme, Field) 30 | parseConfig cfgdir = do 31 | file <- catch (B.readFile $ cfgdir "config.hs") fallback 32 | attrMap <- parseTheme $ cfgdir "theme.hs" 33 | let config = either 34 | (const defaultConfig) 35 | (fromMaybe defaultConfig . readMaybe . unpack) 36 | (decodeUtf8' file) 37 | let db_path = fromMaybe "" (dbPath config) 38 | let kf_path = fromMaybe "" (keyfilePath config) 39 | let ring = if db_path == "" then pathfirst else passwordfirst 40 | let timeout' = timeoutToMaybe $ fromMaybe (Seconds 10) (timeout config) 41 | let should_focus = fromMaybe False (focusSearchOnStart config) 42 | let field_to_focus = if should_focus then SearchField else BrowserField 43 | pure (timeout', db_path, kf_path, ring, attrMap, field_to_focus) 44 | where 45 | pathfirst = F.focusRing [PathField, PasswordField, KeyfileField] 46 | passwordfirst = F.focusRing [PasswordField, KeyfileField, PathField] 47 | timeoutToMaybe (Seconds t) = Just t 48 | timeoutToMaybe DoNotClear = Nothing 49 | 50 | -- type ActualTheme = [(AttrName, ActualAttrVal)] 51 | parseTheme :: FilePath -> IO ActualTheme 52 | parseTheme theme_path = do 53 | file <- catch (B.readFile theme_path) fallback 54 | let theme_aux = either 55 | (const defaultTheme) 56 | (fromMaybe defaultTheme . readMaybe . unpack) 57 | (decodeUtf8' file) 58 | -- bimap f g === (\(a, b) -> (f a, g b)) 59 | pure $ bimap evalName eval <$> theme_aux 60 | -------------------------------------------------------------------------------- /src/kpxhs/Config/Defaults.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE TemplateHaskell #-} 3 | 4 | module Config.Defaults where 5 | 6 | import Data.FileEmbed (embedFile) 7 | import Data.Text (Text) 8 | import Data.Text.Encoding (decodeUtf8) 9 | 10 | import Config.Types 11 | ( Color (Black, Blue, Def, Green, Red, White, Yellow) 12 | , Config (..) 13 | , Style (Bold, Italic) 14 | , Timeout (Seconds) 15 | , UserFacingTheme 16 | , Val (Val, styles) 17 | , bg 18 | , fg 19 | ) 20 | 21 | defaultConfig :: Config 22 | defaultConfig = Config { timeout = Just (Seconds 10) 23 | , dbPath = Nothing 24 | , keyfilePath = Nothing 25 | , focusSearchOnStart = Just False 26 | } 27 | 28 | defaultConfigText :: Text 29 | defaultConfigText = 30 | "Config { timeout = Just (Seconds 10)\n\ 31 | \ , dbPath = Nothing\n\ 32 | \ , keyfilePath = Nothing\n\ 33 | \ , focusSearchOnStart = Just False\n\ 34 | \ }" 35 | 36 | 37 | -- This is the single source of truth for the default theme 38 | -- The docs and defaultThemeText relies on this 39 | -- EMBED START 40 | defaultTheme :: UserFacingTheme 41 | defaultTheme = 42 | [ (["edit"], Val { fg = Black, bg = White, styles = [] }) 43 | , (["edit","focused"], Val { fg = White, bg = Blue, styles = [] }) 44 | , (["dialog"], Val { fg = White, bg = Blue, styles = [] }) 45 | , (["button"], Val { fg = Black, bg = White, styles = [] }) 46 | , (["button","selected"], Val { fg = Def, bg = Yellow, styles = [] }) 47 | , (["progressComplete"], Val { fg = White, bg = Blue, styles = [] }) 48 | , (["kpxhs","key"], Val { fg = Def, bg = White, styles = [] }) 49 | , (["kpxhs","label"], Val { fg = Black, bg = Def, styles = [] }) 50 | , (["kpxhs","line_number"], Val { fg = Yellow, bg = Def, styles = [] }) 51 | , (["kpxhs","line_number","focused"], Val { fg = Red, bg = Def, styles = [Bold]}) 52 | , (["kpxhs","list_border"], Val { fg = Black, bg = Def, styles = [] }) 53 | , (["kpxhs","list_border","focused"], Val { fg = Blue, bg = Def, styles = [] }) 54 | , (["kpxhs","directory"], Val { fg = Black, bg = Def, styles = [Bold]}) 55 | , (["kpxhs","directory","focused"], Val { fg = Red, bg = Def, styles = [Bold]}) 56 | , (["kpxhs","go_up"], Val { fg = Green 57 | , bg = Def 58 | , styles = [Bold, Italic] 59 | }) 60 | , (["kpxhs","go_up","focused"], Val { fg = Blue 61 | , bg = Def 62 | , styles = [Bold, Italic] 63 | }) 64 | , (["kpxhs","entry"], Val { fg = Black, bg = Def, styles = [] }) 65 | , (["kpxhs","entry","focused"], Val { fg = Red, bg = Def, styles = [] }) 66 | ] 67 | -- EMBED END 68 | 69 | defaultThemeText :: Text 70 | defaultThemeText = 71 | -- the docs has to be built BEFORE the program can be compiled 72 | -- Must compile (`make install`) in repo's root dir 73 | -- Warning: if decodeUtf8 fails, it fails at runtime (but that's a big if) 74 | decodeUtf8 $(embedFile "docs/out/default_theme.hs") 75 | -------------------------------------------------------------------------------- /src/kpxhs/Config/Eval.hs: -------------------------------------------------------------------------------- 1 | module Config.Eval (eval, evalName) where 2 | 3 | import qualified Brick as B 4 | import Brick.AttrMap (AttrName, attrName) 5 | import Brick.Util (on) 6 | import Graphics.Vty 7 | ( black 8 | , blink 9 | , blue 10 | , bold 11 | , brightBlack 12 | , brightBlue 13 | , brightCyan 14 | , brightGreen 15 | , brightMagenta 16 | , brightRed 17 | , brightWhite 18 | , brightYellow 19 | , cyan 20 | , dim 21 | , green 22 | , italic 23 | , magenta 24 | , red 25 | , reverseVideo 26 | , rgbColor 27 | , standout 28 | , strikethrough 29 | , underline 30 | , white 31 | , withStyle 32 | , yellow 33 | ) 34 | 35 | import Config.Types 36 | ( ActualAttrVal 37 | , ActualColor 38 | , ActualStyle 39 | , Color (..) 40 | , Style (..) 41 | , UserFacingColor 42 | , UserFacingStyle 43 | , UserFacingVal 44 | , Val (bg, fg, styles) 45 | ) 46 | 47 | evalStyle :: UserFacingStyle -> ActualStyle 48 | evalStyle Standout = standout 49 | evalStyle Underline = underline 50 | evalStyle ReverseVideo = reverseVideo 51 | evalStyle Blink = blink 52 | evalStyle Dim = dim 53 | evalStyle Bold = bold 54 | evalStyle Italic = italic 55 | evalStyle Strikethrough = strikethrough 56 | 57 | -- | Evaluates the colors, especially converting RGB into a Color240 code 58 | -- Note that rgbColor might throw an error; this is intended 59 | evalColor :: UserFacingColor -> Maybe ActualColor 60 | evalColor Black = Just black 61 | evalColor Red = Just red 62 | evalColor Green = Just green 63 | evalColor Yellow = Just yellow 64 | evalColor Blue = Just blue 65 | evalColor Magenta = Just magenta 66 | evalColor Cyan = Just cyan 67 | evalColor White = Just white 68 | evalColor BrightBlack = Just brightBlack 69 | evalColor BrightRed = Just brightRed 70 | evalColor BrightGreen = Just brightGreen 71 | evalColor BrightYellow = Just brightYellow 72 | evalColor BrightBlue = Just brightBlue 73 | evalColor BrightMagenta = Just brightMagenta 74 | evalColor BrightCyan = Just brightCyan 75 | evalColor BrightWhite = Just brightWhite 76 | evalColor (RGB r g b) = Just (rgbColor r g b) 77 | evalColor Def = Nothing 78 | 79 | evalColorAttr :: Maybe ActualColor -> Maybe ActualColor -> ActualAttrVal 80 | evalColorAttr (Just f) (Just b) = f `on` b 81 | evalColorAttr (Just f) _ = B.fg f 82 | evalColorAttr _ (Just b) = B.bg b 83 | evalColorAttr _ _ = mempty 84 | 85 | evalName :: [String] -> AttrName 86 | evalName = foldr (\x acc -> attrName x <> acc) mempty 87 | 88 | eval :: UserFacingVal -> ActualAttrVal 89 | eval r = res 90 | where 91 | mfg = evalColor (fg r) 92 | mbg = evalColor (bg r) 93 | colors = evalColorAttr mfg mbg 94 | g style acc = withStyle acc (evalStyle style) 95 | res = case styles r of 96 | [] -> colors 97 | xs -> foldr g colors xs 98 | -------------------------------------------------------------------------------- /src/kpxhs/Config/Types.hs: -------------------------------------------------------------------------------- 1 | module Config.Types where 2 | 3 | import Brick.AttrMap (AttrName) 4 | import Data.Text (Text) 5 | import Data.Word (Word8) 6 | import qualified Graphics.Vty as V 7 | 8 | -- | Some type aliases to better distinguish user facing types 9 | -- and actual types (user-facing type names are easier to type) 10 | type UserFacingVal = Val 11 | type ActualAttrVal = V.Attr 12 | 13 | type UserFacingColor = Color 14 | type ActualColor = V.Color 15 | 16 | type UserFacingStyle = Style 17 | type ActualStyle = V.Style 18 | 19 | -- | An external representation of the theme 20 | -- (a mapping between attributes and styles) 21 | type UserFacingTheme = [([String], Val)] 22 | 23 | -- | Actual representation of the theme, using Brick types 24 | type ActualTheme = [(AttrName, ActualAttrVal)] 25 | 26 | -- An external representation of an attribute 27 | data Val = 28 | Val { fg :: Color 29 | , bg :: Color 30 | , styles :: [Style] 31 | } 32 | deriving (Show, Read) 33 | 34 | -- | An external representation of either an ISO color (code) or an RGB color 35 | -- Needs to be converted into a Vty Color 36 | -- This is because the Vty Color240 is extremely weird 37 | data Color = Black 38 | | Red 39 | | Green 40 | | Yellow 41 | | Blue 42 | | Magenta 43 | | Cyan 44 | | White 45 | | BrightBlack 46 | | BrightRed 47 | | BrightGreen 48 | | BrightYellow 49 | | BrightBlue 50 | | BrightMagenta 51 | | BrightCyan 52 | | BrightWhite 53 | | RGB Word8 Word8 Word8 54 | | Def 55 | deriving (Show, Read) 56 | 57 | -- | An external representation of the text styles available 58 | data Style = Standout 59 | | Underline 60 | | ReverseVideo 61 | | Blink 62 | | Dim 63 | | Bold 64 | | Italic 65 | | Strikethrough 66 | deriving (Show, Read) 67 | 68 | -- Isomorphic to Maybe, used for users to make the nested maybe 69 | -- less confusing 70 | data Timeout = Seconds Int | DoNotClear 71 | deriving (Show, Read) 72 | 73 | data Config = Config { timeout :: Maybe Timeout 74 | , dbPath :: Maybe Text 75 | , keyfilePath :: Maybe Text 76 | , focusSearchOnStart :: Maybe Bool 77 | } deriving (Show, Read) 78 | -------------------------------------------------------------------------------- /src/kpxhs/Constants.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Constants where 4 | 5 | import Data.Text (Text) 6 | 7 | version :: Text 8 | version = "1.11" 9 | 10 | goUpText :: Text 11 | goUpText = "-- (Go up directory) --" 12 | 13 | help :: String 14 | help = "kpxhs - Interactive Keepass database TUI viewer\n\ 15 | \ Usage\n\ 16 | \ kpxhs Start the program\n\ 17 | \ kpxhs [-v | --version] Print the version number\n\ 18 | \ kpxhs [-h | --help] Show this help\n\ 19 | \ kpxhs --write-config Write the default configs to ~/.config/kpxhs/\n\n\ 20 | \ TUI keybindings (in general)\n\ 21 | \ Esc Quit, back (elsewhere)\n\ 22 | \ q Quit, back (in browser)\n\ 23 | \ Tab Cycle focus\n\ 24 | \ Enter Show entry details\n\ 25 | \ u Copy username\n\ 26 | \ p Copy password\n\n\ 27 | \ Navigation ([n] means optional digit)\n\ 28 | \ [n]j, [n]s Move down n items (default: 1)\n\ 29 | \ [n]k, [n]w Move up n items (default: 1)\n\ 30 | \ g Move to top\n\ 31 | \ G Move to bottom\n\ 32 | \ q Page up\n\ 33 | \ e Page down" 34 | -------------------------------------------------------------------------------- /src/kpxhs/Events.hs: -------------------------------------------------------------------------------- 1 | module Events (appEvent) where 2 | 3 | import Brick.Types (BrickEvent, EventM, Next) 4 | import Lens.Micro ((^.)) 5 | 6 | import Types 7 | ( Event 8 | , Field 9 | , State 10 | , View (BrowserView, EntryDetailsView, ExitDialogView, LoginView, SearchView, LoginFrozenView) 11 | , activeView 12 | ) 13 | import ViewEvents.BrowserEvents.BrowserEvents (browserEvent) 14 | import ViewEvents.EntryDetailsEvents (entryDetailsEvent) 15 | import ViewEvents.ExitDialogEvents (exitEvent) 16 | import ViewEvents.LoginEvents (passwordEvent) 17 | import ViewEvents.LoginFrozenEvents (loginFrozenEvent) 18 | import ViewEvents.SearchEvents (searchEvent) 19 | 20 | 21 | appEvent :: State -> BrickEvent Field Event -> EventM Field (Next State) 22 | appEvent st e = f st e 23 | where 24 | f = case st ^. activeView of 25 | LoginView -> passwordEvent 26 | LoginFrozenView -> loginFrozenEvent 27 | EntryDetailsView -> entryDetailsEvent 28 | SearchView -> searchEvent 29 | BrowserView -> browserEvent 30 | ExitDialogView -> exitEvent 31 | -------------------------------------------------------------------------------- /src/kpxhs/Main.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module Main where 4 | 5 | import qualified Brick.AttrMap as A 6 | import Brick.BChan (BChan, newBChan) 7 | import qualified Brick.Focus as F 8 | import qualified Brick.Main as M 9 | import qualified Brick.Widgets.Edit as E 10 | import Control.Monad (void, when) 11 | import qualified Data.ByteString as B 12 | import qualified Data.Map.Strict as Map 13 | import Data.Text (Text) 14 | import Data.Text.Encoding (encodeUtf8) 15 | import qualified Graphics.Vty as V 16 | import System.Directory 17 | ( createDirectory 18 | , doesDirectoryExist 19 | , doesFileExist 20 | , getHomeDirectory 21 | ) 22 | import System.Environment (getArgs) 23 | import System.Exit (exitFailure) 24 | import System.FilePath (takeFileName, ()) 25 | 26 | import Common (annotate, defaultDialog, initialFooter, toBrowserList) 27 | import Config.Config (parseConfig) 28 | import Config.Defaults (defaultConfigText, defaultThemeText) 29 | import Constants (help, version) 30 | import Events (appEvent) 31 | import Types 32 | import UI (drawUI) 33 | 34 | 35 | initialState :: F.FocusRing Field 36 | -> Text -> Text -> Maybe Int -> BChan Event -> A.AttrMap 37 | -> Field -> State 38 | initialState ring dbdir kfdir timeout' chan theMap field_to_focus = 39 | State 40 | { _visibleEntries = toBrowserList [], 41 | _allEntryNames = Map.empty, 42 | _selectedEntryName = Nothing, 43 | _allEntryDetails = Map.empty, 44 | _previousView = LoginView, -- doesn't really matter here 45 | _activeView = LoginView, 46 | _footer = annotate $ initialFooter ring, 47 | _focusRing = ring, 48 | _dbPathField = E.editor PathField (Just 1) dbdir, 49 | _passwordField = E.editor PasswordField (Just 1) "", 50 | _keyfileField = E.editor KeyfileField (Just 1) kfdir, 51 | _searchField = E.editor SearchField (Just 1) "", 52 | _currentPath = [], 53 | _exitDialog = defaultDialog, 54 | _isClipboardCleared = True, 55 | _chan = chan, 56 | _clearTimeout = timeout', 57 | _countdownThreadId = Nothing, 58 | _counterValue = Nothing, 59 | _currentCmd = "", 60 | _theMap = theMap, 61 | _fieldToFocus = field_to_focus 62 | } 63 | 64 | mkMap :: [(A.AttrName, V.Attr)] -> A.AttrMap 65 | mkMap = A.attrMap V.defAttr 66 | 67 | theApp :: A.AttrMap -> M.App State Event Field 68 | theApp theMap = 69 | M.App 70 | { M.appDraw = drawUI, 71 | M.appChooseCursor = M.showFirstCursor, 72 | M.appHandleEvent = appEvent, 73 | M.appStartEvent = pure, 74 | M.appAttrMap = const theMap 75 | } 76 | 77 | main :: IO () 78 | main = do 79 | args <- getArgs 80 | case args of 81 | [] -> tui 82 | [x] | isCmd "version" x -> print version 83 | [x] | isCmd "help" x -> putStrLn help 84 | [x] | isCmd "write-config" x -> writeConfig 85 | _ -> putStrLn help *> exitFailure 86 | 87 | tui :: IO () 88 | tui = do 89 | home <- getHomeDirectory 90 | let cfgdir = home ".config/kpxhs/" 91 | (timeout', dbdir, kfdir, ring, theme, field) <- parseConfig cfgdir 92 | let theMap = mkMap theme 93 | 94 | chan <- newBChan 10 95 | let buildVty = V.mkVty V.defaultConfig 96 | initialVty <- buildVty 97 | 98 | void $ 99 | M.customMain initialVty buildVty (Just chan) (theApp theMap) 100 | (initialState ring dbdir kfdir timeout' chan theMap field) 101 | 102 | -- `head` is safe because the cmd is hardcoded by me, 103 | -- not passed in by the user 104 | isCmd :: String -> String -> Bool 105 | isCmd cmd string = s == pure (head cmd) || s == cmd 106 | where 107 | s = dropWhile (== '-') string 108 | 109 | -- | Aborts if either config or theme exists, before writing, to prevent inconsistency 110 | -- Logs the write because if one failed and the other succeeded for some reason, 111 | -- the user should be warned that the dir is potentially incomplete/inconsistent 112 | writeConfig :: IO () 113 | writeConfig = do 114 | home <- getHomeDirectory 115 | let cfgdir = home ".config/kpxhs/" 116 | dirExists <- doesDirectoryExist cfgdir 117 | 118 | let cfgPath = cfgdir "config.hs" 119 | let themePath = cfgdir "theme.hs" 120 | if dirExists 121 | then assertFileDoesntExist cfgPath *> assertFileDoesntExist themePath 122 | else createDirectory cfgdir 123 | 124 | B.writeFile cfgPath $ encodeUtf8 defaultConfigText 125 | putStrLn $ "Config written to " <> cfgPath 126 | B.writeFile themePath $ encodeUtf8 defaultThemeText 127 | putStrLn $ "Theme written to " <> themePath 128 | 129 | assertFileDoesntExist :: FilePath -> IO () 130 | assertFileDoesntExist path = do 131 | exists <- doesFileExist path 132 | when exists $ 133 | putStrLn (takeFileName path <> " already exists, aborting") *> exitFailure 134 | -------------------------------------------------------------------------------- /src/kpxhs/Types.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE TemplateHaskell #-} 2 | 3 | module Types where 4 | 5 | import Brick.AttrMap (AttrMap) 6 | import Brick.BChan (BChan) 7 | import qualified Brick.Focus as F 8 | import Brick.Types (Widget) 9 | import qualified Brick.Widgets.Dialog as D 10 | import qualified Brick.Widgets.Edit as E 11 | import qualified Brick.Widgets.List as L 12 | import Control.Concurrent (ThreadId) 13 | import qualified Data.Map.Strict as M 14 | import Data.Text (Text) 15 | import GHC.IO.Exception (ExitCode) 16 | import Lens.Micro.TH (makeLenses) 17 | 18 | 19 | data CmdAction = Ls | Clip | Show 20 | 21 | data View = LoginView 22 | | LoginFrozenView 23 | | BrowserView 24 | | SearchView 25 | | EntryDetailsView 26 | | ExitDialogView 27 | deriving (Eq) 28 | 29 | data Field = PathField 30 | | PasswordField 31 | | KeyfileField 32 | | BrowserField 33 | | SearchField 34 | deriving (Ord, Eq, Show) 35 | 36 | data CopyType = CopyUsername | CopyPassword 37 | 38 | data ExitDialog = Clear | Exit | Cancel 39 | 40 | -- | (exitcode, stdout, stderr) 41 | type CmdOutput = (ExitCode, Text, Text) 42 | 43 | data Event = Login CmdOutput 44 | | EnterDir Text CmdOutput -- ^ Text is the currently selected entry 45 | | ShowEntry Text CmdOutput -- ^ Text is the currently selected entry 46 | | ClearClipCount Int 47 | | Copying (ExitCode, Text) -- ^ Excludes stdout 48 | 49 | data State = State 50 | { -- | The name of visible entries in the current directory 51 | _visibleEntries :: L.List Field Text, 52 | -- | All the entries (visible or not) that has been loaded from all directories 53 | -- Mapping between directory name to list of entry names 54 | _allEntryNames :: M.Map Text [Text], 55 | -- | The name of the entry selected to show details for 56 | _selectedEntryName :: Maybe Text, 57 | -- | All the entry details that has been opened 58 | -- Mapping between directory name to (entry names and their details) 59 | _allEntryDetails :: M.Map Text (M.Map Text Text), 60 | -- | The currently visible View 61 | _activeView :: View, 62 | -- | The previous View 63 | _previousView :: View, 64 | -- | The widget in the bottom of the window 65 | _footer :: Widget Field, 66 | -- | Determines the fields that can be focused and their order 67 | _focusRing :: F.FocusRing Field, 68 | -- | Field for the database path 69 | _dbPathField :: E.Editor Text Field, 70 | -- | Field for the database password 71 | _passwordField :: E.Editor Text Field, 72 | -- | Field for the keyfile path 73 | _keyfileField :: E.Editor Text Field, 74 | -- | Field for the Text in the search bar 75 | _searchField :: E.Editor Text Field, 76 | -- | List of directory names that make up the path of the current directory 77 | _currentPath :: [Text], 78 | -- | The exit dialog 79 | _exitDialog :: D.Dialog ExitDialog, 80 | -- | Whether the clipboard contains a copied value from kpxhs 81 | _isClipboardCleared :: Bool, 82 | -- | The app event channel; contains all the info that needs to be passed from 83 | -- a background thread to the AppEvent handler 84 | _chan :: BChan Event, 85 | -- | Number of seconds to wait before clearing the clipboard 86 | -- If Nothing, then the clipboard won't be automatically cleared 87 | _clearTimeout :: Maybe Int, 88 | -- | The current clipboard clear countdown thread id 89 | _countdownThreadId :: Maybe ThreadId, 90 | -- | The current value of the counter 91 | _counterValue :: Maybe Float, 92 | -- | The current pending vim-like command (how many lines to jump up/down) 93 | -- A String is used instead of Text because it is a [Char] where 94 | -- every Char is the digit that the user pressed 95 | -- There is no need to store the direction/motion command, because 96 | -- as soon as it is pressed, the list can be scrolled and this setting 97 | -- cleared 98 | _currentCmd :: String, 99 | -- | The app's attribute map 100 | _theMap :: AttrMap, 101 | -- | The first field to focus when first logging in 102 | _fieldToFocus :: Field 103 | } 104 | 105 | makeLenses ''State 106 | -------------------------------------------------------------------------------- /src/kpxhs/UI.hs: -------------------------------------------------------------------------------- 1 | module UI (drawUI) where 2 | 3 | import Brick.Types (Widget) 4 | import Lens.Micro ((^.)) 5 | 6 | import Types 7 | ( Field 8 | , State 9 | , View (EntryDetailsView, ExitDialogView, LoginView, LoginFrozenView) 10 | , activeView 11 | ) 12 | import UI.BrowserUI (drawBrowser) 13 | import UI.EntryDetailsUI (drawEntryDetails) 14 | import UI.ExitDialogUI (drawExitDialogView) 15 | import UI.LoginUI (drawDialog) 16 | 17 | 18 | drawUI :: State -> [Widget Field] 19 | drawUI st = case st ^. activeView of 20 | LoginView -> drawDialog st 21 | LoginFrozenView -> drawDialog st 22 | EntryDetailsView -> drawEntryDetails st 23 | ExitDialogView -> drawExitDialogView st 24 | _ -> drawBrowser st 25 | -------------------------------------------------------------------------------- /src/kpxhs/UI/BrowserUI.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module UI.BrowserUI (drawBrowser) where 4 | 5 | import Brick.AttrMap (attrMapLookup) 6 | import qualified Brick.AttrMap as A 7 | import Brick.Markup (markup, (@?)) 8 | import Brick.Types (Padding (Pad), Widget) 9 | import qualified Brick.Widgets.Border as B 10 | import qualified Brick.Widgets.Center as C 11 | import Brick.Widgets.Core 12 | ( hLimitPercent 13 | , padLeft 14 | , str 15 | , txt 16 | , updateAttrMap 17 | , vBox 18 | , vLimitPercent 19 | , (<+>) 20 | ) 21 | import qualified Brick.Widgets.List as L 22 | import Data.Functor ((<&>)) 23 | import Data.Maybe (fromMaybe) 24 | import qualified Data.Text as TT 25 | import qualified Data.Vector as Vec 26 | import Lens.Micro ((&), (^.)) 27 | 28 | import Common (pathToStr) 29 | import Constants (goUpText) 30 | import Types 31 | ( Field 32 | , State 33 | , currentCmd 34 | , currentPath 35 | , footer 36 | , searchField 37 | , theMap 38 | , visibleEntries, View (SearchView, BrowserView), activeView 39 | ) 40 | import qualified Brick.Widgets.Edit as E 41 | 42 | 43 | drawBrowser :: State -> [Widget Field] 44 | drawBrowser st = [ui] 45 | where 46 | ui = 47 | C.vCenter $ 48 | vBox 49 | [ C.hCenter $ drawHeader st, 50 | C.hCenter $ drawBrowserList st, 51 | C.hCenter $ st ^. footer 52 | ] 53 | 54 | drawHeader :: State -> Widget Field 55 | drawHeader st = drawSearchBox st <+> drawCmd st 56 | 57 | drawCmd :: State -> Widget Field 58 | drawCmd st = padLeft (Pad 2) $ str x 59 | where 60 | x = case st^.currentCmd of 61 | "" -> " " 62 | y -> y 63 | 64 | drawSearchBox :: State -> Widget Field 65 | drawSearchBox st = str "Search: " <+> hLimitPercent 75 ed 66 | where 67 | ed = 68 | E.renderEditor 69 | (txt . TT.unlines) (st ^. activeView == SearchView) (st ^. searchField) 70 | 71 | drawBrowserList :: State -> Widget Field 72 | drawBrowserList st = 73 | st & drawBrowserListInner 74 | & vLimitPercent 90 75 | & hLimitPercent 90 76 | & drawBrowserLabel st 77 | & drawBorderColor st 78 | 79 | drawBrowserListInner :: State -> Widget Field 80 | drawBrowserListInner st = 81 | L.renderListWithIndex (drawLine st) True (st^.visibleEntries) 82 | 83 | drawLine :: State -> Int -> Bool -> TT.Text -> Widget n 84 | drawLine st i isCurrent x = num <+> txt " " <+> entry 85 | where 86 | num = drawLineNums st i isCurrent 87 | entry = drawEntry isCurrent x 88 | 89 | drawEntry :: Bool -> TT.Text -> Widget n 90 | drawEntry isCurrent x = res 91 | where 92 | name = case x of 93 | _ | isDir x -> "directory" 94 | _ | isGoUpParent x -> "go_up" 95 | _ -> "entry" 96 | handleCurrent = if isCurrent then name <> "focused" else name 97 | res = markup $ x @? ("kpxhs" <> handleCurrent) 98 | 99 | -- | Differs from ViewEvents.Utils; they take an entire state and 100 | -- lookups the current selection; the ones here has access to the text 101 | isDir :: TT.Text -> Bool 102 | isDir = maybe False ((== '/') . snd) . TT.unsnoc 103 | 104 | isGoUpParent :: TT.Text -> Bool 105 | isGoUpParent = (== goUpText) 106 | 107 | drawLineNums :: State -> Int -> Bool -> Widget n 108 | drawLineNums st i isCurrent = num 109 | where 110 | num = markup $ marker @? ("kpxhs" <> name) 111 | name = if isCurrent then "line_number" <> "focused" else "line_number" 112 | marker = if isCurrent then "> " else diff 113 | diff = 114 | st^.visibleEntries . L.listSelectedL 115 | <&> abs . (i -) 116 | <&> (\d -> (if d >= 10 then "" else " ") <> show d) 117 | <&> TT.pack 118 | & fromMaybe " " 119 | 120 | drawBrowserLabel :: State -> Widget Field -> Widget Field 121 | drawBrowserLabel st = B.borderWithLabel label 122 | where 123 | label = str $ currentPath_ <> " (" <> cur <> "/" <> total <> ")" 124 | currentPath_ = 125 | case pathToStr $ st^.currentPath of 126 | "" -> "(Root)" 127 | x -> TT.unpack x 128 | cur = maybe "-" (show . (+1)) (st^.visibleEntries.L.listSelectedL) 129 | total = show $ Vec.length $ st^.visibleEntries.L.listElementsL 130 | 131 | drawBorderColor :: State -> Widget Field -> Widget Field 132 | drawBorderColor st = res 133 | where 134 | name = case st ^. activeView of 135 | BrowserView -> "list_border" <> "focused" 136 | _ -> "list_border" 137 | borderColor = attrMapLookup ("kpxhs" <> name) $ st^.theMap 138 | res = updateAttrMap (A.applyAttrMappings [(B.borderAttr, borderColor)]) 139 | -------------------------------------------------------------------------------- /src/kpxhs/UI/Common.hs: -------------------------------------------------------------------------------- 1 | module UI.Common (getEditor) where 2 | 3 | import qualified Brick.Focus as F 4 | import Brick.Types (Widget) 5 | import Brick.Widgets.Core (txt) 6 | import qualified Brick.Widgets.Edit as E 7 | import Data.Text (Text) 8 | import Lens.Micro (Getting, (^.)) 9 | 10 | import Types (Field, State, focusRing) 11 | 12 | 13 | getEditor :: State 14 | -> Getting (E.Editor Text Field) State (E.Editor Text Field) 15 | -> ([Text] -> Text) 16 | -> Widget Field 17 | getEditor st f g = 18 | F.withFocusRing (st ^. focusRing) (E.renderEditor (txt . g)) (st ^. f) 19 | -------------------------------------------------------------------------------- /src/kpxhs/UI/EntryDetailsUI.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE FlexibleContexts #-} 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module UI.EntryDetailsUI (drawEntryDetails) where 5 | 6 | import Brick.Types (Widget) 7 | import qualified Brick.Widgets.Center as C 8 | import Brick.Widgets.Core (str, txt, vBox) 9 | import Brick.Widgets.Table (renderTable, table) 10 | import Data.Maybe (fromMaybe) 11 | import Data.Text (Text) 12 | import qualified Data.Text as TT 13 | import Lens.Micro ((^.)) 14 | 15 | import Common (maybeGetEntryData) 16 | import Types (Field, State, footer) 17 | 18 | 19 | drawEntryDetails :: State -> [Widget Field] 20 | drawEntryDetails st = 21 | [ C.center $ vBox $ fromMaybe def (drawEntryDetailsInner st) ] 22 | where 23 | def = [ C.hCenter $ str "Failed to get entry!" ] 24 | 25 | drawEntryDetailsInner :: State -> Maybe [Widget Field] 26 | drawEntryDetailsInner st = do 27 | entryData <- maybeGetEntryData st 28 | let tableWidget = drawTable entryData 29 | pure [ C.hCenter $ renderTable $ table tableWidget, 30 | C.hCenter $ st ^. footer ] 31 | 32 | drawTable :: Text -> [[Widget Field]] 33 | drawTable raw = 34 | case TT.splitOn "Notes: " raw of 35 | [rest, notes] -> drawTableWithNotes rest notes 36 | _ -> drawTableWithoutNotes raw 37 | 38 | drawTableWithNotes :: Text -> Text -> [[Widget Field]] 39 | drawTableWithNotes rest notes = restRows ++ notesRow 40 | where 41 | restRows = drawTableWithoutNotes rest 42 | -- To avoid splitting by ": " inside note contents 43 | notesRow = [[ txt "Notes", txt $ replaceEmpty notes ]] 44 | 45 | -- For some reason the Notes section is missing 46 | drawTableWithoutNotes :: Text -> [[Widget Field]] 47 | drawTableWithoutNotes raw = rows 48 | where 49 | xs = TT.splitOn ": " <$> TT.lines raw 50 | rows = (txt . replaceEmpty <$>) <$> xs 51 | 52 | replaceEmpty :: Text -> Text 53 | replaceEmpty s = 54 | case s of 55 | "" -> "-" 56 | x -> x 57 | -------------------------------------------------------------------------------- /src/kpxhs/UI/ExitDialogUI.hs: -------------------------------------------------------------------------------- 1 | module UI.ExitDialogUI (drawExitDialogView) where 2 | 3 | import Brick.Types (Widget) 4 | import qualified Brick.Widgets.Center as C 5 | import Brick.Widgets.Core (padAll, str) 6 | import qualified Brick.Widgets.Dialog as D 7 | import Lens.Micro ((^.)) 8 | 9 | import Types (Field, State, exitDialog) 10 | 11 | 12 | drawExitDialogView :: State -> [Widget Field] 13 | drawExitDialogView st = [ui] 14 | where 15 | ui = D.renderDialog (st^.exitDialog) 16 | $ C.hCenter 17 | $ padAll 1 18 | $ str "Do you want to clear the clipboard before exiting?" 19 | -------------------------------------------------------------------------------- /src/kpxhs/UI/LoginUI.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module UI.LoginUI (drawDialog) where 4 | 5 | import Brick.Types (Widget) 6 | import qualified Brick.Widgets.Center as C 7 | import Brick.Widgets.Core (hLimitPercent, str, vBox, (<+>)) 8 | import Data.Text (Text) 9 | import qualified Data.Text as TT 10 | import Lens.Micro ((^.)) 11 | 12 | import Constants (version) 13 | import Types 14 | ( Field 15 | , State 16 | , dbPathField 17 | , footer 18 | , keyfileField 19 | , passwordField 20 | ) 21 | import UI.Common (getEditor) 22 | import Brick (txt) 23 | 24 | 25 | hidePassword :: [Text] -> Text 26 | hidePassword xs = TT.replicate (TT.length (TT.unlines xs) - 1) "*" 27 | 28 | drawDialog :: State -> [Widget Field] 29 | drawDialog st = [ui] 30 | where 31 | e1 = getEditor st dbPathField TT.unlines 32 | e2 = getEditor st passwordField hidePassword 33 | e3 = getEditor st keyfileField TT.unlines 34 | ui = 35 | C.vCenter $ 36 | vBox 37 | [ 38 | C.hCenter $ txt $ "kpxhs v" <> version <> " (GPLv3)", 39 | C.hCenter $ str " ", 40 | C.hCenter $ str "File: " <+> hLimitPercent 75 e1, 41 | C.hCenter $ str " ", 42 | C.hCenter $ str "Password: " <+> hLimitPercent 75 e2, 43 | C.hCenter $ str " ", 44 | C.hCenter $ str "Keyfile: " <+> hLimitPercent 75 e3, 45 | C.hCenter $ str " ", 46 | C.hCenter $ st ^. footer 47 | ] 48 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/BrowserEvents/BrowserEvents.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module ViewEvents.BrowserEvents.BrowserEvents (browserEvent) where 4 | 5 | import qualified Brick.Main as M 6 | import qualified Brick.Types as T 7 | import Brick.Widgets.Core (str) 8 | import Brick.Widgets.List (listMoveToBeginning, listMoveToEnd) 9 | import qualified Brick.Widgets.List as L 10 | import Data.Char (isDigit) 11 | import Data.Maybe (fromMaybe) 12 | import qualified Graphics.Vty as V 13 | import Lens.Micro ((%~), (&), (.~), (^.)) 14 | 15 | import Types 16 | ( CopyType (CopyPassword, CopyUsername) 17 | , Event 18 | , Field 19 | , State 20 | , currentCmd 21 | , currentPath 22 | , footer 23 | , isClipboardCleared 24 | , visibleEntries 25 | ) 26 | import ViewEvents.BrowserEvents.Core (goUpParent) 27 | import ViewEvents.BrowserEvents.Event (handleAppEvent) 28 | import ViewEvents.BrowserEvents.Fork (handleEnter) 29 | import ViewEvents.BrowserEvents.Utils (listMovePageDown, listMovePageUp) 30 | import ViewEvents.BrowserEvents.VimCommand (handleVimDigit, handleVimMotion) 31 | import ViewEvents.Common 32 | ( commonTabEvent 33 | , liftContinue 34 | , prepareExit 35 | , updateFooterGuarded 36 | ) 37 | import ViewEvents.Copy (copyEntryCommon) 38 | import ViewEvents.Utils (getSelectedEntry, isCopyable) 39 | 40 | browserEvent :: State -> T.BrickEvent Field Event -> T.EventM Field (T.Next State) 41 | browserEvent = 42 | commonTabEvent 43 | ( \st e -> 44 | case e of 45 | T.VtyEvent (V.EvKey V.KEsc []) -> handleEsc st 46 | T.VtyEvent (V.EvKey (V.KChar 'q') []) -> handleQuit st 47 | T.VtyEvent (V.EvKey V.KEnter []) -> handleEnter st 48 | T.VtyEvent (V.EvKey (V.KChar 'p') []) | isCopyable st -> copyPassword st 49 | T.VtyEvent (V.EvKey (V.KChar 'u') []) | isCopyable st -> copyUsername st 50 | T.VtyEvent ev -> M.continue $ handleNav ev st 51 | T.AppEvent ev -> liftContinue $ handleAppEvent st ev 52 | _ -> M.continue st 53 | ) 54 | 55 | handleEsc :: State -> T.EventM Field (T.Next State) 56 | handleEsc st = 57 | M.continue $ case st^.currentCmd of 58 | "" -> st 59 | _ -> st & currentCmd .~ "" 60 | 61 | handleQuit :: State -> T.EventM Field (T.Next State) 62 | handleQuit st = 63 | case (st^.currentPath, st^.isClipboardCleared) of 64 | ([], False) -> M.continue $ prepareExit st 65 | ([], True) -> M.halt st 66 | _ -> M.continue $ goUpParent st 67 | 68 | copyPassword :: State -> T.EventM Field (T.Next State) 69 | copyPassword st = liftContinue $ copyEntryFromBrowser st CopyPassword 70 | 71 | copyUsername :: State -> T.EventM Field (T.Next State) 72 | copyUsername st = liftContinue $ copyEntryFromBrowser st CopyUsername 73 | 74 | copyEntryFromBrowser :: State -> CopyType -> IO State 75 | copyEntryFromBrowser st ctype = 76 | fromMaybe def (getSelectedEntry f st) 77 | where 78 | def = pure $ st & footer .~ str "No entry selected!" 79 | f entry = copyEntryCommon st entry ctype 80 | 81 | handleNav :: V.Event -> State -> State 82 | handleNav e st = new_st & updateFooterGuarded 83 | where 84 | noCmd = null $ st ^. currentCmd 85 | f x = st & visibleEntries %~ x 86 | g x = f x & currentCmd .~ "" 87 | new_st = 88 | case e of 89 | -- Keys from handleListEvent (not affected by vim commands) 90 | V.EvKey V.KUp [] -> g L.listMoveUp 91 | V.EvKey V.KDown [] -> g L.listMoveDown 92 | V.EvKey V.KHome [] -> g listMoveToBeginning 93 | V.EvKey V.KEnd [] -> g listMoveToEnd 94 | V.EvKey V.KPageDown [] -> g listMovePageDown 95 | V.EvKey V.KPageUp [] -> g listMovePageUp 96 | -- Additional keys that are not affected by vim commands 97 | V.EvKey (V.KChar 'g') [] -> g listMoveToBeginning 98 | V.EvKey (V.KChar 'G') [] -> g listMoveToEnd 99 | -- Keys that take an optional count before them 100 | V.EvKey (V.KChar 'w') [] | noCmd -> f L.listMoveUp 101 | V.EvKey (V.KChar 's') [] | noCmd -> f L.listMoveDown 102 | V.EvKey (V.KChar 'k') [] | noCmd -> f L.listMoveUp 103 | V.EvKey (V.KChar 'j') [] | noCmd -> f L.listMoveDown 104 | -- Vim commands 105 | V.EvKey (V.KChar x) [] | isDigit x -> handleVimDigit st x 106 | V.EvKey (V.KChar x) [] -> handleVimMotion st x 107 | _ -> st 108 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/BrowserEvents/Core.hs: -------------------------------------------------------------------------------- 1 | -- | Functions that all browser event module uses 2 | -- The core functionality of show entry, enter dir, and go up parent are here 3 | -- Plus some extra utils they use 4 | {-# LANGUAGE OverloadedStrings #-} 5 | 6 | module ViewEvents.BrowserEvents.Core where 7 | 8 | import qualified Brick.Widgets.Edit as E 9 | import Data.Functor ((<&>)) 10 | import Data.Map.Strict ((!?)) 11 | import qualified Data.Map.Strict as M 12 | import Data.Maybe (fromMaybe) 13 | import Data.Text (Text) 14 | import Lens.Micro ((%~), (&), (.~), (<>~), (?~), (^.)) 15 | 16 | import Common 17 | ( maybeGetEntryData 18 | , pathToStr 19 | , pathToStrRoot 20 | , toBrowserList 21 | ) 22 | import Types 23 | ( Field (SearchField) 24 | , State 25 | , View (EntryDetailsView) 26 | , activeView 27 | , allEntryDetails 28 | , allEntryNames 29 | , currentPath 30 | , searchField 31 | , selectedEntryName 32 | , visibleEntries 33 | ) 34 | import ViewEvents.Common (updateFooter) 35 | 36 | 37 | maybeGetEntries :: State -> Maybe [Text] 38 | maybeGetEntries st = 39 | (st ^. allEntryNames) !? dir 40 | where 41 | newDir = initOrDef ["."] (st ^. currentPath) 42 | dir = pathToStr newDir 43 | 44 | initOrDef :: [a] -> [a] -> [a] 45 | initOrDef d [] = d 46 | initOrDef d [_] = d 47 | initOrDef _ xs = init xs 48 | 49 | showEntryWithCache :: State -> Text -> Maybe State 50 | showEntryWithCache st entryname = 51 | maybeGetEntryData st <&> showEntryInner st entryname 52 | 53 | showEntrySuccess :: State -> Text -> Text -> State 54 | showEntrySuccess st entry stdout = 55 | case maybeGetEntryData st of 56 | Just x -> showEntryInner st entry x 57 | Nothing -> showEntryInner st entry stdout 58 | 59 | showEntryInner :: State -> Text -> Text -> State 60 | showEntryInner st entry details = newst 61 | where 62 | dirname = pathToStrRoot (st^.currentPath) 63 | f :: Maybe (M.Map Text Text) -> Maybe (M.Map Text Text) 64 | f (Just m) = Just $ M.insertWith (curry snd) entry details m 65 | f _ = Just $ M.singleton entry details 66 | newst = st & activeView .~ EntryDetailsView 67 | & selectedEntryName ?~ entry 68 | & allEntryDetails %~ M.alter f dirname 69 | & updateFooter 70 | -- Not guarded here because the countdown should only be in 71 | -- browser view 72 | 73 | -- allEntryNames is updated here only, so that we can search inside the folder 74 | enterDirSuccess :: State -> [Text] -> Text -> State 75 | enterDirSuccess st entries_ rawDir = 76 | st & visibleEntries .~ toBrowserList entries_ 77 | & allEntryNames %~ M.insert rawDir entries_ 78 | & searchField .~ E.editor SearchField (Just 1) "" 79 | & currentPath <>~ [rawDir] 80 | & updateFooter -- clears any footers set when entering dir 81 | 82 | goUpParent :: State -> State 83 | goUpParent st = 84 | st & visibleEntries .~ toBrowserList entries 85 | & searchField .~ E.editor SearchField (Just 1) "" 86 | & currentPath %~ initOrDef [] 87 | & updateFooter 88 | where 89 | entries = fromMaybe ["Failed to get entries!"] $ maybeGetEntries st 90 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/BrowserEvents/Event.hs: -------------------------------------------------------------------------------- 1 | -- | Functions that handles custom AppEvents (that the browser should handle), 2 | -- such as the completion of a shell command, or the copy countdown 3 | 4 | module ViewEvents.BrowserEvents.Event (handleAppEvent) where 5 | 6 | import Brick.Widgets.Core (txt) 7 | import Data.Text (Text) 8 | import GHC.IO.Exception (ExitCode (ExitSuccess)) 9 | import Lens.Micro ((&), (.~)) 10 | 11 | import Constants (goUpText) 12 | import Types 13 | ( CmdOutput 14 | , Event (Copying, EnterDir, ShowEntry) 15 | , State 16 | , footer 17 | ) 18 | import ViewEvents.BrowserEvents.Core (enterDirSuccess, showEntrySuccess) 19 | import ViewEvents.Copy (handleCopy) 20 | import ViewEvents.Utils (processStdout) 21 | 22 | handleAppEvent :: State -> Event -> IO State 23 | handleAppEvent st (ShowEntry entry out) = pure $ handleShowEntryEvent st entry out 24 | handleAppEvent st (EnterDir entry out) = pure $ handleEnterDirEvent st entry out 25 | handleAppEvent st (Copying e) = handleCopy st e 26 | handleAppEvent st _ = pure st 27 | 28 | handleEnterDirEvent :: State -> Text -> CmdOutput -> State 29 | handleEnterDirEvent st entry (ExitSuccess, stdout, _) = 30 | enterDirSuccess st (goUpText : processStdout stdout) entry 31 | handleEnterDirEvent st _ (_, _, stderr) = 32 | st & footer .~ txt stderr 33 | 34 | handleShowEntryEvent :: State -> Text -> CmdOutput -> State 35 | handleShowEntryEvent st entry (ExitSuccess, stdout, _) = showEntrySuccess st entry stdout 36 | handleShowEntryEvent st _ (_, _, stderr) = st & footer .~ txt stderr 37 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/BrowserEvents/Fork.hs: -------------------------------------------------------------------------------- 1 | -- | Functions that run a shell command in the background ("Fork") 2 | -- (After trying the cache first) 3 | -- The output of the shell command will be handled later, 4 | -- asynchronously, by functions in ViewEvents.BrowserEvents.Event 5 | {-# LANGUAGE OverloadedStrings #-} 6 | 7 | module ViewEvents.BrowserEvents.Fork (handleEnter) where 8 | 9 | import Brick.BChan (writeBChan) 10 | import qualified Brick.Types as T 11 | import Brick.Widgets.Core (str, txt) 12 | import Control.Concurrent (forkIO) 13 | import Control.Monad (void) 14 | import Data.Map.Strict ((!?)) 15 | import Data.Maybe (fromMaybe) 16 | import Data.Text (Text) 17 | import Lens.Micro ((&), (.~), (?~), (^.)) 18 | 19 | import Common (pathToStr) 20 | import Constants (goUpText) 21 | import Types 22 | ( CmdAction (Ls, Show) 23 | , Event (EnterDir, ShowEntry) 24 | , Field 25 | , State 26 | , allEntryNames 27 | , chan 28 | , currentPath 29 | , footer 30 | , selectedEntryName 31 | ) 32 | import ViewEvents.BrowserEvents.Core 33 | ( enterDirSuccess 34 | , goUpParent 35 | , showEntryWithCache 36 | ) 37 | import ViewEvents.Common (liftContinue) 38 | import ViewEvents.Utils (getCreds, getSelectedEntry, isDir, runCmd) 39 | 40 | handleEnter :: State -> T.EventM Field (T.Next State) 41 | handleEnter st = liftContinue $ f st 42 | where 43 | f = if isDir st then enterDirFork else showEntryFork 44 | 45 | showEntryFork :: State -> IO State 46 | showEntryFork st = fromMaybe def (getSelectedEntry f st) 47 | where 48 | def = pure $ st & footer .~ str "No entry selected!" 49 | f x = if x == goUpText 50 | then pure $ goUpParent st 51 | else fetchEntryInBackground st x 52 | 53 | fetchEntryInBackground :: State -> Text -> IO State 54 | fetchEntryInBackground st entry = maybe def pure (showEntryWithCache newst entry) 55 | where 56 | newst = st & selectedEntryName ?~ entry 57 | (dir, pw, kf) = getCreds newst 58 | bg_cmd = do 59 | (code, stdout, stderr) <- runCmd Show dir [entry] pw kf 60 | writeBChan (newst^.chan) $ ShowEntry entry (code, stdout, stderr) 61 | def = do 62 | void $ forkIO bg_cmd 63 | pure $ newst & footer .~ txt "Fetching..." 64 | 65 | 66 | enterDirFork :: State -> IO State 67 | enterDirFork st = fromMaybe def (getSelectedEntry f st) 68 | where 69 | def = pure $ st & footer .~ str "No directory selected!" 70 | f = fetchDirInBackground st 71 | 72 | fetchDirInBackground :: State -> Text -> IO State 73 | fetchDirInBackground st entry = 74 | case (st ^. allEntryNames) !? entry of 75 | Just x -> pure $ enterDirSuccess st x entry 76 | Nothing -> do 77 | void $ forkIO bg_cmd 78 | pure $ st & footer .~ txt "Fetching..." 79 | where 80 | (dbPathField_, pw, kf) = getCreds st 81 | concatedDir = pathToStr (st ^. currentPath) <> entry 82 | bg_cmd = do 83 | (code, stdout, stderr) <- runCmd Ls dbPathField_ [concatedDir] pw kf 84 | writeBChan (st^.chan) $ EnterDir entry (code, stdout, stderr) 85 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/BrowserEvents/Utils.hs: -------------------------------------------------------------------------------- 1 | -- | Utils to navigate around the list 2 | 3 | module ViewEvents.BrowserEvents.Utils 4 | ( listMoveWith 5 | , listMovePageUp 6 | , listMovePageDown ) where 7 | 8 | import Brick.Util (clamp) 9 | import qualified Brick.Widgets.List as L 10 | import Data.Maybe (fromMaybe) 11 | 12 | listMoveWith :: (L.Splittable t, Foldable t) 13 | => (Int -> Int) -> L.GenericList n t a -> L.GenericList n t a 14 | listMoveWith f l = L.listMoveTo clamped l 15 | where 16 | clamped = clamp 0 (length $ L.listElements l) num 17 | num = f (fromMaybe 0 $ L.listSelected l) 18 | 19 | -- Default page up and down functions too fast for me 20 | listMovePageUp :: (L.Splittable t, Foldable t) 21 | => L.GenericList n t a -> L.GenericList n t a 22 | listMovePageUp = listMoveWith (subtract 5) 23 | 24 | listMovePageDown :: (L.Splittable t, Foldable t) 25 | => L.GenericList n t a -> L.GenericList n t a 26 | listMovePageDown = listMoveWith (5 +) 27 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/BrowserEvents/VimCommand.hs: -------------------------------------------------------------------------------- 1 | module ViewEvents.BrowserEvents.VimCommand (handleVimDigit, handleVimMotion) where 2 | 3 | import Brick.Types (Direction (Down, Up)) 4 | import Data.Function ((&)) 5 | import Lens.Micro ((%~), (.~), (<>~), (^.)) 6 | import Text.Read (readMaybe) 7 | 8 | import Types (State, currentCmd, visibleEntries) 9 | import ViewEvents.BrowserEvents.Utils (listMoveWith) 10 | import ViewEvents.Common (updateFooterGuarded) 11 | 12 | handleVimDigit :: State -> Char -> State 13 | handleVimDigit st x = 14 | st & currentCmd <>~ [x] 15 | 16 | toDirection :: Char -> Maybe Direction 17 | toDirection 's' = Just Down 18 | toDirection 'j' = Just Down 19 | toDirection 'w' = Just Up 20 | toDirection 'k' = Just Up 21 | toDirection _ = Nothing 22 | 23 | handleVimMotion :: State -> Char -> State 24 | handleVimMotion st c = 25 | case (mnum, msign) of 26 | (Just num, Just sign) -> st & currentCmd .~ "" 27 | & visibleEntries %~ listMoveWith (sign num) 28 | & updateFooterGuarded 29 | _ -> st 30 | where 31 | mnum = case st ^. currentCmd of 32 | [] -> Nothing 33 | xs -> readMaybe xs 34 | msign = case toDirection c of 35 | Just Down -> Just (+) 36 | Just Up -> Just subtract 37 | Nothing -> Nothing 38 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/Common.hs: -------------------------------------------------------------------------------- 1 | -- Functions for further composition and combining 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module ViewEvents.Common where 5 | 6 | import qualified Brick.Main as M 7 | import Brick.Types (Widget) 8 | import qualified Brick.Types as T 9 | import Control.Monad.IO.Class (liftIO) 10 | import Data.Maybe (isJust) 11 | import qualified Graphics.Vty as V 12 | import Lens.Micro ((&), (.~), (^.)) 13 | 14 | import Common (annotate, defaultDialog, initialFooter) 15 | import Types 16 | ( Event (ClearClipCount) 17 | , Field 18 | , State 19 | , View (BrowserView, EntryDetailsView, ExitDialogView, LoginView, SearchView, LoginFrozenView) 20 | , activeView 21 | , countdownThreadId 22 | , currentPath 23 | , exitDialog 24 | , focusRing 25 | , footer 26 | , previousView 27 | ) 28 | import ViewEvents.Copy (handleClipCount) 29 | import ViewEvents.Utils (isCopyable) 30 | 31 | liftContinue :: IO b -> T.EventM n (T.Next b) 32 | liftContinue g = liftIO g >>= M.continue 33 | 34 | prepareExit :: State -> State 35 | prepareExit st = 36 | st & previousView .~ (st^.activeView) 37 | & exitDialog .~ defaultDialog 38 | & activeView .~ ExitDialogView 39 | 40 | commonTabEvent :: (State -> T.BrickEvent Field Event -> T.EventM Field (T.Next State)) 41 | -> State 42 | -> T.BrickEvent Field Event 43 | -> T.EventM Field (T.Next State) 44 | commonTabEvent fallback st e = 45 | case e of 46 | T.VtyEvent (V.EvKey (V.KChar '/') []) -> M.continue $ focusSearch st 47 | T.AppEvent (ClearClipCount count) -> liftContinue $ handleClipCount st count 48 | _ -> fallback st e 49 | 50 | focusSearch :: State -> State 51 | focusSearch st = 52 | st & activeView .~ SearchView 53 | & updateFooterGuarded 54 | 55 | -- | Restores the default footer for the current view 56 | -- Should be only used when transitioning to a new view or field 57 | updateFooter :: State -> State 58 | updateFooter st = st & footer .~ viewDefaultFooter st 59 | 60 | updateFooterGuarded :: State -> State 61 | updateFooterGuarded st = 62 | if isJust $ st ^. countdownThreadId 63 | then st 64 | else st & updateFooter 65 | 66 | viewDefaultFooter :: State -> Widget Field 67 | viewDefaultFooter st = 68 | annotate $ case st^.activeView of 69 | SearchView -> [esc " focus list "] 70 | EntryDetailsView -> [back, username, password] 71 | LoginView -> initialFooter $ st ^. focusRing 72 | LoginFrozenView -> initialFooter $ st ^. focusRing 73 | ExitDialogView -> [("", "")] 74 | BrowserView -> 75 | let extra = if isCopyable st then [view_details, username, password] else [open_folder] in 76 | case st^.currentPath of 77 | [] -> [exitq, focus_search] <> extra 78 | _ -> [backq, focus_search] <> extra 79 | where 80 | esc x = ("Esc", x) 81 | exitq = ("q", " exit ") 82 | back = esc " back " 83 | backq = ("q", " back ") 84 | username = ("u", " copy username ") 85 | password = ("p", " copy password") 86 | focus_search = ("/", " search ") 87 | view_details = ("Enter", " details ") 88 | open_folder = ("Enter", " open folder ") 89 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/Copy.hs: -------------------------------------------------------------------------------- 1 | -- Handles all copying and clipboard matters 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module ViewEvents.Copy where 5 | 6 | import Brick.BChan (writeBChan) 7 | import Brick.Widgets.Core (txt) 8 | import qualified Brick.Widgets.ProgressBar as P 9 | import Control.Concurrent (forkIO, threadDelay) 10 | import Control.Monad (void) 11 | import Data.Function ((&)) 12 | import Data.Text (Text) 13 | import GHC.Conc (killThread) 14 | import GHC.IO.Exception (ExitCode (ExitSuccess)) 15 | import Lens.Micro ((.~), (?~), (^.)) 16 | import System.Exit (ExitCode (ExitFailure)) 17 | import System.Info (os) 18 | import System.Process (callCommand) 19 | 20 | import Types 21 | ( CmdAction (Clip) 22 | , CopyType (CopyUsername) 23 | , Event (ClearClipCount, Copying) 24 | , State 25 | , View (BrowserView, EntryDetailsView, SearchView) 26 | , activeView 27 | , chan 28 | , clearTimeout 29 | , countdownThreadId 30 | , counterValue 31 | , footer 32 | , isClipboardCleared 33 | ) 34 | import ViewEvents.Utils (getCreds, runCmd) 35 | import Data.Functor (($>)) 36 | import Data.Foldable (forM_) 37 | 38 | 39 | _copyTypeToStr :: CopyType -> Text 40 | _copyTypeToStr CopyUsername = "username" 41 | _copyTypeToStr _ = "password" 42 | 43 | copyEntryCommon :: State -> Text -> CopyType -> IO State 44 | copyEntryCommon st entry ctype = do 45 | let (dir, pw, kf) = getCreds st 46 | let attr = _copyTypeToStr ctype 47 | void $ forkIO $ do 48 | (code, _, stderr) <- runCmd Clip dir [entry, "-a", attr, "0"] pw kf 49 | writeBChan (st^.chan) $ Copying (code, stderr) 50 | pure $ st & footer .~ txt "Copying..." 51 | 52 | handleCopy :: State -> (ExitCode, Text) -> IO State 53 | handleCopy st (ExitFailure _, stderr) = pure $ st & footer .~ txt stderr 54 | handleCopy st (ExitSuccess, _) = 55 | case st ^. clearTimeout of 56 | Nothing -> pure $ st & footer .~ txt "Copied!" 57 | Just timeout' -> handleCopyInner st timeout' 58 | 59 | handleCopyInner :: State -> Int -> IO State 60 | handleCopyInner st timeout' = do 61 | -- If there already exists a countdown thread, kill it first to 62 | -- prevent interference 63 | forM_ (st ^. countdownThreadId) killThread 64 | -- Save the tid in case if it needs to be cancelled later 65 | tid <- forkIO $ writeBChan (st^.chan) $ ClearClipCount timeout' 66 | pure $ st & isClipboardCleared .~ False 67 | & countdownThreadId ?~ tid 68 | 69 | handleClipCount :: State -> Int -> IO State 70 | handleClipCount st 0 = 71 | clearClipboard $> 72 | (st & footer .~ txt "Clipboard cleared" 73 | & isClipboardCleared .~ True 74 | & countdownThreadId .~ Nothing 75 | & counterValue .~ Nothing) 76 | handleClipCount st count = 77 | case st ^. clearTimeout of 78 | Nothing -> pure st 79 | Just timeout' -> handleClipCountInner st count timeout' 80 | 81 | -- Even if the footer shouldn't be changed (eg, in another view), 82 | -- the countdown should proceed, because if the view is updated to something 83 | -- where the footer should change, then the progress bar should appear again 84 | handleClipCountInner :: State -> Int -> Int -> IO State 85 | handleClipCountInner st count timeout' = do 86 | -- Save the tid in case if it needs to be cancelled later 87 | tid <- forkIO $ do 88 | threadDelay 1000000 89 | writeBChan (st^.chan) (ClearClipCount (count - 1)) 90 | pure $ st & f 91 | & countdownThreadId ?~ tid 92 | & counterValue ?~ v 93 | where 94 | label = mkCountdownLabel count 95 | -- https://github.com/NorfairKing/haskell-dangerous-functions#fromintegral 96 | -- I think Int -> Float is fine because Float is larger than Int 97 | -- so an int shouldn't be truncated 98 | v = fromIntegral count / fromIntegral timeout' 99 | f = if st^.activeView `elem` [BrowserView, SearchView, EntryDetailsView] 100 | then footer .~ P.progressBar label v 101 | else id 102 | 103 | mkCountdownLabel :: Show a => a -> Maybe String 104 | mkCountdownLabel count = 105 | Just $ "Clearing clipboard in " <> show count <> " seconds" 106 | 107 | clearClipboard :: IO () 108 | clearClipboard = callCommand $ "printf '' | " ++ handler 109 | where 110 | handler = case os of 111 | "linux" -> "xclip -selection clipboard" 112 | _ -> "pbcopy" 113 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/EntryDetailsEvents.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module ViewEvents.EntryDetailsEvents (entryDetailsEvent) where 4 | 5 | import qualified Brick.Main as M 6 | import qualified Brick.Types as T 7 | import Brick.Widgets.Core (str) 8 | import qualified Brick.Widgets.ProgressBar as P 9 | import Data.Maybe (fromMaybe) 10 | import qualified Graphics.Vty as V 11 | import Lens.Micro ((&), (.~), (^.)) 12 | 13 | import Types 14 | ( CopyType (CopyPassword, CopyUsername) 15 | , Event (ClearClipCount, Copying) 16 | , Field 17 | , State 18 | , View (BrowserView) 19 | , activeView 20 | , clearTimeout 21 | , counterValue 22 | , footer 23 | , selectedEntryName 24 | ) 25 | import ViewEvents.Common (liftContinue, updateFooterGuarded) 26 | import ViewEvents.Copy 27 | ( copyEntryCommon 28 | , handleClipCount 29 | , handleCopy 30 | , mkCountdownLabel 31 | ) 32 | 33 | entryDetailsEvent :: State -> T.BrickEvent Field Event -> T.EventM Field (T.Next State) 34 | entryDetailsEvent st (T.VtyEvent e) = 35 | case e of 36 | V.EvKey V.KEsc [] -> M.continue $ returnToBrowser st 37 | V.EvKey (V.KChar 'p') [] -> liftContinue $ copyEntryFromDetails st CopyPassword 38 | V.EvKey (V.KChar 'u') [] -> liftContinue $ copyEntryFromDetails st CopyUsername 39 | _ -> M.continue st 40 | entryDetailsEvent st (T.AppEvent (ClearClipCount count)) = 41 | liftContinue $ handleClipCount st count 42 | entryDetailsEvent st (T.AppEvent (Copying ev)) = liftContinue $ handleCopy st ev 43 | entryDetailsEvent st _ = M.continue st 44 | 45 | returnToBrowser :: State -> State 46 | returnToBrowser st = 47 | st & activeView .~ BrowserView 48 | & f 49 | where 50 | -- https://github.com/NorfairKing/haskell-dangerous-functions#fromintegral 51 | -- I think Int -> Float is fine because Float is larger than Int 52 | -- so an int shouldn't be truncated 53 | toCount :: Float -> Int -> Int 54 | toCount x t = round $ x * fromIntegral t 55 | f = case (st^.counterValue, st^.clearTimeout) of 56 | (Just x, Just t) -> footer .~ P.progressBar (mkCountdownLabel $ toCount x t) x 57 | _ -> updateFooterGuarded 58 | 59 | copyEntryFromDetails :: State -> CopyType -> IO State 60 | copyEntryFromDetails st ctype = fromMaybe def (maybeCopy st ctype) 61 | where 62 | def = pure $ st & footer .~ str "Failed to get entry name or details!" 63 | 64 | maybeCopy :: State -> CopyType -> Maybe (IO State) 65 | maybeCopy st ctype = 66 | case st ^. selectedEntryName of 67 | Just entry -> Just $ copyEntryCommon st entry ctype 68 | Nothing -> Nothing 69 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/ExitDialogEvents.hs: -------------------------------------------------------------------------------- 1 | module ViewEvents.ExitDialogEvents (exitEvent) where 2 | 3 | import qualified Brick.Main as M 4 | import qualified Brick.Types as T 5 | import Brick.Widgets.Dialog (handleDialogEvent) 6 | import qualified Brick.Widgets.Dialog as D 7 | import Control.Monad.IO.Class (MonadIO (liftIO)) 8 | import Data.Functor ((<&>)) 9 | import qualified Graphics.Vty as V 10 | import Lens.Micro ((&), (.~), (^.)) 11 | 12 | import Types 13 | ( ExitDialog (Cancel, Clear, Exit) 14 | , Field 15 | , State 16 | , activeView 17 | , exitDialog 18 | , previousView 19 | ) 20 | import ViewEvents.Common (updateFooter) 21 | import ViewEvents.Copy (clearClipboard) 22 | 23 | 24 | exitEvent :: State -> T.BrickEvent Field e -> T.EventM Field (T.Next State) 25 | exitEvent st (T.VtyEvent (V.EvKey V.KEnter [])) = handleEnter st 26 | exitEvent st (T.VtyEvent e) = handleDialog st e 27 | exitEvent st _ = M.continue st 28 | 29 | handleDialog :: State -> V.Event -> T.EventM Field (T.Next State) 30 | handleDialog st e = 31 | handleDialogEventEsc e st >>= M.continue 32 | 33 | handleDialogEventEsc :: V.Event -> State -> T.EventM n State 34 | handleDialogEventEsc ev st = 35 | case ev of 36 | V.EvKey V.KEsc [] -> pure $ cancelExit st 37 | _ -> handleDialogEvent ev (st^.exitDialog) <&> setDialog 38 | where 39 | -- handleDialogEvent returns EventM (Dialog a) 40 | -- setDialog transforms that inner Dialog back into a State 41 | setDialog :: D.Dialog ExitDialog -> State 42 | setDialog x = st & exitDialog .~ x 43 | 44 | cancelExit :: State -> State 45 | cancelExit st = st & activeView .~ (st^.previousView) 46 | & updateFooter 47 | 48 | handleEnter :: State -> T.EventM Field (T.Next State) 49 | handleEnter st = 50 | case D.dialogSelection (st^.exitDialog) of 51 | Just Clear -> liftIO clearClipboard *> M.halt st 52 | Just Cancel -> M.continue $ cancelExit st 53 | Just Exit -> M.halt st 54 | _ -> M.continue st 55 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/LoginEvents.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | {-# LANGUAGE Rank2Types #-} 3 | 4 | module ViewEvents.LoginEvents (passwordEvent) where 5 | 6 | import Brick.BChan (writeBChan) 7 | import qualified Brick.Focus as F 8 | import qualified Brick.Main as M 9 | import qualified Brick.Types as T 10 | import Brick.Widgets.Core (txt) 11 | import qualified Brick.Widgets.Edit as E 12 | import Control.Concurrent (forkIO) 13 | import Control.Monad (void) 14 | import Data.Text (Text) 15 | import qualified Data.Text as TT 16 | import qualified Graphics.Vty as V 17 | import Lens.Micro (Lens', (%~), (&), (.~), (^.)) 18 | 19 | import Types 20 | ( CmdAction (Ls) 21 | , Event (Login) 22 | , Field (KeyfileField, PasswordField, PathField) 23 | , State 24 | , View (LoginFrozenView) 25 | , activeView 26 | , chan 27 | , dbPathField 28 | , focusRing 29 | , footer 30 | , keyfileField 31 | , passwordField 32 | ) 33 | import ViewEvents.Common (liftContinue, updateFooter) 34 | import ViewEvents.Utils (getCreds, runCmd) 35 | 36 | 37 | valid :: State -> Bool 38 | valid st = f $ getCreds st 39 | where 40 | f (a, b, _) = not (TT.null a && TT.null b) 41 | 42 | passwordEvent :: State -> T.BrickEvent n Event -> T.EventM Field (T.Next State) 43 | passwordEvent st (T.VtyEvent e) = 44 | case e of 45 | V.EvKey V.KEsc [] -> M.halt st 46 | V.EvKey (V.KChar '\t') [] -> focus F.focusNext st 47 | V.EvKey V.KBackTab [] -> focus F.focusPrev st 48 | V.EvKey V.KEnter [] | valid st -> liftContinue $ loginInBackground st 49 | _ -> M.continue =<< handleFieldInput st e 50 | passwordEvent st _ = M.continue st 51 | 52 | focus :: (F.FocusRing Field -> F.FocusRing Field) 53 | -> State 54 | -> T.EventM Field (T.Next State) 55 | focus f st = M.continue $ st & focusRing %~ f 56 | & updateFooter 57 | 58 | loginInBackground :: State -> IO State 59 | loginInBackground st = do 60 | let (dir, pw, kf) = getCreds st 61 | void $ forkIO $ do 62 | (code, stdout, stderr) <- runCmd Ls dir [] pw kf 63 | writeBChan (st^.chan) $ Login (code, stdout, stderr) 64 | pure $ st & activeView .~ LoginFrozenView 65 | & footer .~ txt "Logging in..." 66 | 67 | handleFieldInput :: State -> V.Event -> T.EventM Field State 68 | handleFieldInput st e = 69 | case F.focusGetCurrent (st ^. focusRing) of 70 | Just PathField -> inner dbPathField 71 | Just PasswordField -> inner passwordField 72 | Just KeyfileField -> inner keyfileField 73 | _ -> pure st 74 | where 75 | inner :: Lens' State (E.Editor Text Field) -> T.EventM Field State 76 | inner field_ = T.handleEventLensed st field_ E.handleEditorEvent e 77 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/LoginFrozenEvents.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module ViewEvents.LoginFrozenEvents (loginFrozenEvent) where 4 | 5 | import qualified Brick.Main as M 6 | import qualified Brick.Types as T 7 | import Brick.Widgets.Core (txt) 8 | import qualified Data.Map.Strict as Map 9 | import Data.Text (Text) 10 | import Lens.Micro ((&), (.~), (^.)) 11 | import System.Exit (ExitCode (ExitSuccess)) 12 | 13 | import Common (toBrowserList) 14 | import Types 15 | ( Event (Login) 16 | , Field (SearchField) 17 | , State 18 | , activeView 19 | , allEntryNames 20 | , footer 21 | , visibleEntries 22 | , View (BrowserView, SearchView, LoginView), fieldToFocus 23 | ) 24 | import ViewEvents.Common (updateFooter) 25 | import ViewEvents.Utils (processStdout) 26 | 27 | loginFrozenEvent :: State -> T.BrickEvent Field Event -> T.EventM Field (T.Next State) 28 | loginFrozenEvent st (T.AppEvent e) = M.continue $ gotoBrowser st e 29 | loginFrozenEvent st _ = M.continue st 30 | 31 | gotoBrowser :: State -> Event -> State 32 | gotoBrowser st (Login (ExitSuccess, stdout, _)) = gotoBrowserSuccess st 33 | $ processStdout stdout 34 | gotoBrowser st (Login (_, _, stderr)) = loginFail st stderr 35 | gotoBrowser st _ = st 36 | 37 | gotoBrowserSuccess :: State -> [Text] -> State 38 | gotoBrowserSuccess st ent = 39 | st & activeView .~ view 40 | & visibleEntries .~ toBrowserList ent 41 | & allEntryNames .~ Map.singleton "." ent 42 | & updateFooter 43 | where 44 | field = st ^. fieldToFocus 45 | view = if field == SearchField then SearchView else BrowserView 46 | 47 | loginFail :: State -> Text -> State 48 | loginFail st stderr = 49 | st & footer .~ txt stderr 50 | & activeView .~ LoginView 51 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/SearchEvents.hs: -------------------------------------------------------------------------------- 1 | {-# LANGUAGE OverloadedStrings #-} 2 | 3 | module ViewEvents.SearchEvents (searchEvent) where 4 | 5 | import qualified Brick.Main as M 6 | import qualified Brick.Types as T 7 | import Brick.Widgets.Core (txt) 8 | import qualified Brick.Widgets.Edit as E 9 | import Data.Map.Strict ((!?)) 10 | import Data.Maybe (fromMaybe, listToMaybe) 11 | import Data.Text (Text, isInfixOf, toLower) 12 | import qualified Graphics.Vty as V 13 | import Lens.Micro ((&), (.~), (^.)) 14 | 15 | import Common (toBrowserList) 16 | import Types 17 | ( Event 18 | , Field 19 | , State 20 | , allEntryNames 21 | , currentPath 22 | , searchField 23 | , visibleEntries, footer, View(BrowserView), activeView 24 | ) 25 | import ViewEvents.Common (commonTabEvent, updateFooterGuarded) 26 | 27 | 28 | searchEvent :: State -> T.BrickEvent Field Event -> T.EventM Field (T.Next State) 29 | searchEvent = 30 | commonTabEvent 31 | ( \st e -> 32 | case e of 33 | T.VtyEvent (V.EvKey V.KEsc []) -> M.continue $ exitSearch st 34 | T.VtyEvent ev -> M.continue =<< handleSearch st ev 35 | _ -> M.continue st 36 | ) 37 | 38 | exitSearch :: State -> State 39 | exitSearch st = 40 | st & activeView .~ BrowserView 41 | & updateFooterGuarded 42 | 43 | handleSearch :: State -> V.Event -> T.EventM Field State 44 | handleSearch st e = do 45 | field <- E.handleEditorEvent e (st ^. searchField) 46 | let updatedSt = st & searchField .~ field 47 | searchStr = toLower $ fromMaybe "" $ listToMaybe $ E.getEditContents field 48 | f entry = searchStr `isInfixOf` toLower entry 49 | g = case (st ^. allEntryNames) !? theDir of 50 | Nothing -> footer .~ txt "Failed to get entries!" 51 | Just x -> visibleEntries .~ toBrowserList (filter f x) 52 | pure $ updatedSt & g 53 | where 54 | theDir = dirsToCurrent $ st ^. currentPath 55 | 56 | 57 | -- Adapted from the definition of last in Prelude 58 | dirsToCurrent :: [Text] -> Text 59 | dirsToCurrent [] = "." 60 | dirsToCurrent [x] = x 61 | dirsToCurrent (_:xs) = dirsToCurrent xs 62 | -------------------------------------------------------------------------------- /src/kpxhs/ViewEvents/Utils.hs: -------------------------------------------------------------------------------- 1 | -- Functions that do simple calculations 2 | {-# LANGUAGE OverloadedStrings #-} 3 | 4 | module ViewEvents.Utils where 5 | 6 | import qualified Brick.Widgets.Edit as E 7 | import Data.List (partition, sort) 8 | import Data.Text (Text) 9 | import qualified Data.Text as TT 10 | import qualified Data.Text.Zipper as Z 11 | import Lens.Micro ((^.)) 12 | import System.Process (readProcessWithExitCode) 13 | 14 | import qualified Brick.Widgets.List as L 15 | import Constants (goUpText) 16 | import Data.Maybe (fromMaybe) 17 | import Types 18 | ( CmdAction (..) 19 | , CmdOutput 20 | , Field 21 | , State 22 | , dbPathField 23 | , keyfileField 24 | , passwordField 25 | , visibleEntries 26 | ) 27 | 28 | 29 | getSelectedEntry :: (Text -> a) -> State -> Maybe a 30 | getSelectedEntry f st = do 31 | (_, entry) <- L.listSelectedElement $ st ^. visibleEntries 32 | pure $ f entry 33 | 34 | processStdout :: Text -> [Text] 35 | processStdout s = dirs ++ entries_ 36 | where 37 | (dirs, entries_) = partition ("/" `TT.isSuffixOf`) x 38 | x = sort $ TT.lines s 39 | 40 | getCreds :: State -> (Text, Text, Text) 41 | getCreds st = (dir, pw, kf) 42 | where 43 | dir = extractTextField $ st ^. dbPathField 44 | pw = extractTextField $ st ^. passwordField 45 | kf = extractTextField $ st ^. keyfileField 46 | extractTextField :: E.Editor Text Field -> Text 47 | extractTextField field = 48 | let res = Z.getText $ field ^. E.editContentsL in 49 | case res of 50 | [] -> "" 51 | (x : _) -> x 52 | 53 | isDir :: State -> Bool 54 | isDir st = fromMaybe False (getSelectedEntry f st) 55 | where 56 | -- snd <$> TT.unsnoc is like a maybeLast function 57 | f = maybe False ((== '/') . snd) . TT.unsnoc 58 | 59 | isGoUpToParent :: State -> Bool 60 | isGoUpToParent st = fromMaybe False (getSelectedEntry f st) 61 | where 62 | f = (== goUpText) 63 | 64 | isCopyable :: State -> Bool 65 | isCopyable st = not (isDir st || isGoUpToParent st) 66 | 67 | actionToString :: CmdAction -> String 68 | actionToString Ls = "ls" 69 | actionToString Clip = "clip" 70 | actionToString Show = "show" 71 | 72 | runCmd :: CmdAction 73 | -> Text -- ^ dir 74 | -> [Text] -- ^ args 75 | -> Text -- ^ password 76 | -> Text -- ^ keyfile path 77 | -> IO CmdOutput 78 | runCmd a = _runCmdInner (actionToString a) 79 | 80 | _runCmdInner :: String 81 | -> Text 82 | -> [Text] 83 | -> Text 84 | -> Text 85 | -> IO CmdOutput 86 | _runCmdInner action dir extraArgs pw kf = do 87 | (code, stdout, stderr) <- readProcessWithExitCode "keepassxc-cli" args (TT.unpack pw) 88 | pure (code, TT.pack stdout, TT.pack stderr) 89 | where 90 | args :: [String] 91 | args = [action, TT.unpack dir] ++ extraArgs_ 92 | extraArgs_ = 93 | TT.unpack <$> case kf of 94 | "" -> extraArgs 95 | _ -> ["-k", kf] ++ extraArgs 96 | -------------------------------------------------------------------------------- /stack.yaml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by 'stack init' 2 | # 3 | # Some commonly used options have been documented as comments in this file. 4 | # For advanced use and comprehensive documentation of the format, please see: 5 | # https://docs.haskellstack.org/en/stable/yaml_configuration/ 6 | 7 | # Resolver to choose a 'specific' stackage snapshot or a compiler version. 8 | # A snapshot resolver dictates the compiler version and the set of packages 9 | # to be used for project dependencies. For example: 10 | # 11 | # resolver: lts-3.5 12 | # resolver: nightly-2015-09-21 13 | # resolver: ghc-7.10.2 14 | # 15 | # The location of a snapshot can be provided as a file or url. Stack assumes 16 | # a snapshot provided as a file might change, whereas a url resource does not. 17 | # 18 | # resolver: ./custom-snapshot.yaml 19 | # resolver: https://example.com/snapshots/2018-01-01.yaml 20 | resolver: 21 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/27.yaml 22 | 23 | # User packages to be built. 24 | # Various formats can be used as shown in the example below. 25 | # 26 | # packages: 27 | # - some-directory 28 | # - https://example.com/foo/bar/baz-0.0.2.tar.gz 29 | # subdirs: 30 | # - auto-update 31 | # - wai 32 | packages: 33 | - . 34 | # Dependency packages to be pulled from upstream that are not in the resolver. 35 | # These entries can reference officially published versions as well as 36 | # forks / in-progress versions pinned to a git hash. For example: 37 | # 38 | # extra-deps: 39 | # - acme-missiles-0.3 40 | # - git: https://github.com/commercialhaskell/stack.git 41 | # commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a 42 | # 43 | extra-deps: [ 44 | ] 45 | 46 | # Override default flag values for local packages and extra-deps 47 | # flags: {} 48 | 49 | # Extra package databases containing global packages 50 | # extra-package-dbs: [] 51 | 52 | # Control whether we use the GHC we find on the path 53 | # system-ghc: true 54 | # 55 | # Require a specific version of stack, using version ranges 56 | # require-stack-version: -any # Default 57 | # require-stack-version: ">=2.5" 58 | # 59 | # Override the architecture used by stack, especially useful on Windows 60 | # arch: i386 61 | # arch: x86_64 62 | # 63 | # Extra directories used by stack for building 64 | # extra-include-dirs: [/path/to/dir] 65 | # extra-lib-dirs: [/path/to/dir] 66 | # 67 | # Allow a newer minor version of GHC than the snapshot specifies 68 | # compiler-check: newer-minor 69 | -------------------------------------------------------------------------------- /stack.yaml.lock: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by Stack. 2 | # You should not edit this file by hand. 3 | # For more information, please see the documentation at: 4 | # https://docs.haskellstack.org/en/stable/lock_files 5 | 6 | packages: [] 7 | snapshots: 8 | - completed: 9 | size: 590102 10 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/27.yaml 11 | sha256: 79a786674930a89301b0e908fad2822a48882f3d01486117693c377b8edffdbe 12 | original: 13 | url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/27.yaml 14 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | password for both databases is `asd` 2 | -------------------------------------------------------------------------------- /test/keyfile.key: -------------------------------------------------------------------------------- 1 | hello world 2 | -------------------------------------------------------------------------------- /test/kpxhs_test.kdbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akazukin5151/kpxhs/683f71ae8fa5a77c47f38df68133b1cf251b8238/test/kpxhs_test.kdbx -------------------------------------------------------------------------------- /test/kpxhs_test_no_keyfile.kdbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/akazukin5151/kpxhs/683f71ae8fa5a77c47f38df68133b1cf251b8238/test/kpxhs_test_no_keyfile.kdbx --------------------------------------------------------------------------------