Full Code of codecentric/gopass-ui for AI

master 6f4bcd3f7609 cached
93 files
136.7 KB
33.7k tokens
129 symbols
1 requests
Download .txt
Repository: codecentric/gopass-ui
Branch: master
Commit: 6f4bcd3f7609
Files: 93
Total size: 136.7 KB

Directory structure:
gitextract_1wdh2ba1/

├── .github/
│   └── workflows/
│       └── on-push.yml
├── .gitignore
├── .huskyrc
├── .nvmrc
├── .prettierrc
├── .vscode/
│   └── launch.json
├── LICENSE
├── README.md
├── docs/
│   ├── development.md
│   ├── platforms-and-packages.md
│   └── releasing.md
├── mocks/
│   ├── fileMock.js
│   └── styleMock.js
├── package.json
├── src/
│   ├── main/
│   │   ├── AppUtilities.ts
│   │   ├── AppWindows.ts
│   │   ├── GopassExecutor.ts
│   │   └── index.ts
│   ├── renderer/
│   │   ├── common/
│   │   │   ├── Settings.ts
│   │   │   └── notifications/
│   │   │       ├── Notification.tsx
│   │   │       └── NotificationProvider.tsx
│   │   ├── components/
│   │   │   ├── ExternalLink.tsx
│   │   │   ├── GoBackNavigationButton.tsx
│   │   │   ├── PaginatedTable.tsx
│   │   │   ├── RoundActionButton.tsx
│   │   │   ├── loading-screen/
│   │   │   │   ├── LoadingScreen.css
│   │   │   │   └── LoadingScreen.tsx
│   │   │   └── tree/
│   │   │       ├── TreeComponent.tsx
│   │   │       ├── TreeHeader.tsx
│   │   │       └── TreeStyle.ts
│   │   ├── explorer-app/
│   │   │   ├── ExplorerApplication.css
│   │   │   ├── ExplorerApplication.tsx
│   │   │   ├── GithubService.ts
│   │   │   ├── MainContent.tsx
│   │   │   ├── SecretsProvider.tsx
│   │   │   ├── components/
│   │   │   │   ├── EnvironmentTest.tsx
│   │   │   │   ├── LastVersionInfo.tsx
│   │   │   │   ├── MainNavigation.tsx
│   │   │   │   └── PasswordStrengthInfo.tsx
│   │   │   ├── pages/
│   │   │   │   ├── AddMountPage.tsx
│   │   │   │   ├── AddSecretPage.css
│   │   │   │   ├── AddSecretPage.tsx
│   │   │   │   ├── HomePage.tsx
│   │   │   │   ├── MountsPage.tsx
│   │   │   │   ├── PasswordHealthPage.tsx
│   │   │   │   ├── SettingsPage.tsx
│   │   │   │   └── details/
│   │   │   │       ├── HistoryTable.tsx
│   │   │   │       └── SecretDetailsPage.tsx
│   │   │   ├── password-health/
│   │   │   │   ├── PasswordHealthIndicator.tsx
│   │   │   │   ├── PasswordHealthRules.ts
│   │   │   │   ├── PasswordRater.ts
│   │   │   │   ├── PasswordRatingComponent.css
│   │   │   │   ├── PasswordRatingComponent.tsx
│   │   │   │   └── PasswordRule.d.ts
│   │   │   └── side-navigation/
│   │   │       ├── SecretExplorer.tsx
│   │   │       ├── SecretTree.tsx
│   │   │       ├── SecretsDirectoryService.ts
│   │   │       └── SecretsFilterService.ts
│   │   ├── explorer-app.tsx
│   │   ├── search-app/
│   │   │   ├── CollectionItems.tsx
│   │   │   ├── SearchApplication.css
│   │   │   ├── SearchApplication.tsx
│   │   │   ├── SearchResults.tsx
│   │   │   ├── SearchResultsView.tsx
│   │   │   └── SecretText.tsx
│   │   ├── search-app.tsx
│   │   ├── secrets/
│   │   │   ├── AsyncPasswordHealthCollector.ts
│   │   │   ├── Gopass.ts
│   │   │   ├── deriveIconFromSecretName.ts
│   │   │   └── useCopySecretToClipboard.ts
│   │   └── types/
│   │       ├── electron-is-accelerator.d.ts
│   │       ├── fallback.d.ts
│   │       ├── promise-timeout.d.ts
│   │       └── string-replace-to-array.d.ts
│   └── shared/
│       └── settings.ts
├── test/
│   ├── Gopass.test.ts
│   ├── PasswordHealthIndicator.test.ts
│   ├── SecretsDirectoryService.test.ts
│   ├── SecretsFilterService.test.ts
│   ├── __snapshots__/
│   │   └── SecretsDirectoryService.test.ts.snap
│   ├── deriveIconFromSecretName.test.ts
│   └── mock/
│       └── electron-mock.ts
├── tsconfig.json
├── tslint.json
├── webpack.base.config.js
├── webpack.main.config.js
├── webpack.main.prod.config.js
├── webpack.renderer.explorer.config.js
├── webpack.renderer.explorer.dev.config.js
├── webpack.renderer.explorer.prod.config.js
├── webpack.renderer.search.config.js
├── webpack.renderer.search.dev.config.js
└── webpack.renderer.search.prod.config.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/on-push.yml
================================================
name: On push (tests, build)
on: push
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '14.16'
          registry-url: 'https://registry.npmjs.org'
      - run: npm i
      - run: npm test

  build:
    runs-on: ${{ matrix.os }}

    strategy:
      matrix:
        os: [macos-latest, ubuntu-latest, windows-latest]

    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-node@v2
        with:
          node-version: '14.16'
          registry-url: 'https://registry.npmjs.org'
      - name: Build/release Electron app
        uses: samuelmeuli/action-electron-builder@v1
        with:
          # GitHub token, automatically provided to the action (no need to define this secret in the repo settings)
          github_token: ${{ secrets.github_token }}

          # this action will not release
          release: false


================================================
FILE: .gitignore
================================================
node_modules/

.idea/
*.iml

dist/
release/


================================================
FILE: .huskyrc
================================================
{
  "hooks": {
    "pre-commit": "npm run lint && npm test"
  }
}


================================================
FILE: .nvmrc
================================================
14.16


================================================
FILE: .prettierrc
================================================
{
    "singleQuote": true,
    "jsxSingleQuote": true,
    "arrowParens": "avoid",
    "printWidth": 160,
    "tabWidth": 4,
    "semi": false,
    "trailingComma": "none",
    "bracketSpacing": true
}


================================================
FILE: .vscode/launch.json
================================================
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "program": "${workspaceFolder}/dist/main.js"
        }
    ]
}

================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2019 codecentric AG

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# Gopass UI [![Latest release](https://img.shields.io/github/release/codecentric/gopass-ui.svg)](https://github.com/codecentric/gopass-ui/releases/latest)

<img src="docs/img/gopass-ui-logo.png" alt="Gopass UI logo" style="max-width: 800px">

## What is Gopass and Gopass UI?

> [Gopass](https://github.com/gopasspw/gopass) is a rewrite of the pass password manager in Go with the aim of making it cross-platform and adding additional features – (Taken from Github)

`Gopass UI` is wrapping `gopass` from your command line. It makes your life easier by providing a graphical user interface to search and manage your secrets. It looks like this:

<img src="docs/img/demo-720p.gif" alt="GIF demonstrating core features of Gopass UI" title="Gopass UI demo" style="max-width: 720px" />

In addition there is a search window that can be opened with `(cmd || ctrl) + shift + p`.

## How can I use it?

For most platforms just [download the latest packaged application](https://github.com/codecentric/gopass-ui/releases/latest) from our releases and install it. We provide binaries for MacOS, Windows and Linux (deb, RPM, snap, AppImage, Gentoo). For more details see [supported platforms and packages](docs/platforms-and-packages.md).

Of course, you need to have [Gopass](https://github.com/gopasspw/gopass) up and running. We also recommend to use a dialog-based PIN-entry-tool for typing in GPG passphrases like [pinentry-mac for MacOS](https://formulae.brew.sh/formula/pinentry-mac) instead of using the default terminal-based.

### Platform notice

We'll only test the MacOS builds and **are not able to offer support for Linux and Windows releases**. We are happy to review your pull requests addressing any of such issues.  

## Issues and Contribution

Feel free to report any usage issue. We are very keen about your feedback and appreciate any help.
You'd like to help us developing Gopass UI? Awesome! We are looking forward to your pull requests, issues and participation in discussion.

## Development

See how to get started in our [development documentation](docs/development.md).


================================================
FILE: docs/development.md
================================================
## Development

### Clone and install dependencies

First, clone the repository and navigate inside:

```bash
git clone https://github.com/codecentric/gopass-ui.git && cd gopass-ui/
```

Then, install the dependencies:

```bash
nvm use # make sure that nvm is installed on your machine and it installs the requested Node version
npm install
```

### Development

The app is divided into one main process and two renderer processes. One renderer process is for the global search window, the other one for the main explorer window.
All processes have to be started **simultaneously** in different console tabs:

```bash
# don't forget nvm use && npm install from the previous section ;-)

# run this in a pane for powering the main process (the "backend")
npm run start-main-dev
 # run this in a pane for the renderer of the main/explorer window
npm run start-renderer-explorer-dev
# run this in a pane for the renderer of the search window
npm run start-renderer-search-dev
```

This will start the application with hot-reloading so you can instantly start developing and see the changes in the open application.

### Testing

We use Jest for tests. Currently the project contains (too less) unit and integration tests. Unit tests should have no dependency to the local machine except the Node environment we're setting up. Integration tests can also involve system binaries like Gopass, GPG and so on – you got the point ;-)

Run them with `npm test` and `npm run test:integration`.


### Linting

This project contains `prettier` and `tslint`. **TLDR:** Prettier assists during development. Tslint is ran in a Husky pre-commit hook together with unit tests and in Github actions (see `.github/workflows/`).

**Prettier** is more aggressive because it is designed opinionated. It will find and correct more . The only options we decide on are to be found in `.prettierrc`. We use Prettier to enforce and apply code style during development process. Right after saving an edited file it will correct code style mistakes! In VSCode this comes with the Prettier extension already. In JetBrains IntelliJ IDEA/Webstorm [this can be easily configured](https://prettier.io/docs/en/webstorm.html). On the CLI, feel free to use `npm run prettier:check` and `npm run prettier:write`.

**Tslint** is used to check the code style baseline before commiting staged code and while running CI. Feel free to use the scripts `npm run lint` for linting and `npm run lint:fix` for fixing simple issues automatically to make code comply to the baseline style.


### Production packaging

We use [Electron builder](https://www.electron.build/) to build and package the application. See how we use it to package all targeted platforms and formats in [our release docs](./releasing.md).

Packaging will create all results in `releases` folder.


================================================
FILE: docs/platforms-and-packages.md
================================================
## Supported Platforms

Gopass-ui is available for the following platforms:
* MacOS (.dmg)
* Windows (.exe)
* Linux, see next section

### Linux Packages

For Linux the following packages are provided:
* .deb (download [here](https://github.com/codecentric/gopass-ui/releases/latest))
* .rpm (download [here](https://github.com/codecentric/gopass-ui/releases/latest))
* .snap (download [here](https://github.com/codecentric/gopass-ui/releases/latest))
* .pacman (download [here](https://github.com/codecentric/gopass-ui/releases/latest))
* .AppImage (download [here](https://github.com/codecentric/gopass-ui/releases/latest))
* Gentoo: `emerge app-admin/gopass-ui` ([gentoo overlay](https://gitlab.awesome-it.de/overlays/awesome), thanks [@danielcb](https://github.com/danielcb))


================================================
FILE: docs/releasing.md
================================================
## Releasing and Publishing Gopass UI

This documents the steps it needs to release and publish a new version of Gopass UI to Github.

### In the codebase

1. Let's check if code style and tests are okay: `npm run release:check`. If there are issues, fix them first.
2. Increment the version number in `package.json` and do `npm i` to reflect it within `package-lock.json`.
3. Build the releases for your local platform to verify everything is working: `npm run release`. This takes a while. If successful, the binaries were built in `release/`
4. Build the releases for [all supported platforms and packages](./platforms-and-packages.md): `npm run release:full`
5. As we know that everything worked, commit and push the version change

### Draft and public release on Github

1. [Draft a new release](https://github.com/codecentric/gopass-ui/releases/new)
2. Choose the created Git tag.
3. Write a precise but catchy release title. Maybe something about the core topics of this release etc.
4. Describe this release in detail. What features were added or changed? Were bugs fixed? New platforms supported?
5. Attach all binaries for this release from the `release/` directory.
6. Publish and spread the word! 🎉🎉🎉


================================================
FILE: mocks/fileMock.js
================================================
module.exports = 'test-file-stub'


================================================
FILE: mocks/styleMock.js
================================================
module.exports = {}


================================================
FILE: package.json
================================================
{
  "name": "gopass-ui",
  "version": "0.8.0",
  "description": "Awesome UI for the gopass CLI – a password manager for your daily business",
  "main": "./dist/main.js",
  "scripts": {
    "build-main": "cross-env NODE_ENV=production webpack --config webpack.main.prod.config.js",
    "build-renderer-search": "cross-env NODE_ENV=production webpack --config webpack.renderer.search.prod.config.js",
    "build-renderer-explorer": "cross-env NODE_ENV=production webpack --config webpack.renderer.explorer.prod.config.js",
    "build": "npm run build-main && npm run build-renderer-explorer && npm run build-renderer-search",
    "start-renderer-search-dev": "NODE_OPTIONS=\"--max-old-space-size=2048\" webpack-dev-server --config webpack.renderer.search.dev.config.js",
    "start-renderer-explorer-dev": "NODE_OPTIONS=\"--max-old-space-size=2048\" webpack-dev-server --config webpack.renderer.explorer.dev.config.js",
    "start-main-dev": "webpack --config webpack.main.config.js && electron ./dist/main.js",
    "prestart": "npm run build",
    "start": "electron .",
    "prettier:check": "prettier --check '{src,test,mocks}/**/*.{ts,tsx,js,jsx}'",
    "prettier:write": "prettier --write '{src,test,mocks}/**/*.{ts,tsx,js,jsx}'",
    "lint": "tslint '{src,test,mocks}/**/*.{ts,tsx,js,jsx}' --project ./tsconfig.json",
    "lint:fix": "tslint '{src,test,mocks}/**/*.{ts,tsx}' --project ./tsconfig.json --fix",
    "test": "npm run test:unit",
    "test:unit": "jest --testRegex '\\.test\\.tsx?$'",
    "test:unit:watch": "jest --testRegex '\\.test\\.tsx?$' --watch",
    "test:integration": "jest --testRegex '\\.itest\\.ts$'",
    "release:check": "npm run lint && npm test",
    "release": "npm run release:check && npm run build && electron-builder --publish onTag",
    "release:full": "npm run release:check && npm run build && electron-builder --mac dmg --win --linux deb rpm snap AppImage pacman",
    "postinstall": "electron-builder install-app-deps"
  },
  "jest": {
    "transform": {
      "^.+\\.tsx?$": "ts-jest"
    },
    "testRegex": "\\.?test\\.tsx?$",
    "moduleFileExtensions": [
      "ts",
      "tsx",
      "js",
      "json",
      "node"
    ],
    "moduleNameMapper": {
      "\\.(jpg|jpeg|png|gif|eot|otf|webp|svg|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga)$": "<rootDir>/mocks/fileMock.js",
      "\\.(s?css|sass)$": "<rootDir>/mocks/styleMock.js",
      "^electron$": "<rootDir>/test/mock/electron-mock.ts"
    }
  },
  "build": {
    "productName": "Gopass UI",
    "appId": "de.codecentric.gopassui",
    "directories": {
      "output": "release"
    },
    "files": [
      "dist/",
      "node_modules/",
      "package.json"
    ],
    "mac": {
      "category": "public.app-category.productivity"
    },
    "publish": [
      "github"
    ]
  },
  "repository": {
    "type": "git",
    "url": "git+ssh://git@github.com:codecentric/gopass-ui.git"
  },
  "author": {
    "name": "codecentric AG",
    "email": "info@codecentric.de",
    "url": "https://www.codecentric.de"
  },
  "contributors": [
    {
      "name": "Matthias Rütten",
      "email": "matthias.ruetten@codecentric.de"
    },
    {
      "name": "Jonas Verhoelen",
      "email": "jonas.verhoelen@codecentric.de"
    }
  ],
  "license": "SEE LICENSE",
  "bugs": {
    "url": "https://github.com/codecentric/gopass-ui/issues"
  },
  "homepage": "https://github.com/codecentric/gopass-ui",
  "devDependencies": {
    "@types/dateformat": "^3.0.1",
    "@types/electron-devtools-installer": "2.2.0",
    "@types/electron-settings": "3.1.2",
    "@types/history": "^4.7.2",
    "@types/jest": "^26.0.23",
    "@types/lodash": "^4.14.170",
    "@types/node": "^14.17.4",
    "@types/react": "^16.8.13",
    "@types/react-dom": "^16.8.3",
    "@types/react-hot-loader": "^4.1.0",
    "@types/react-router": "^4.4.5",
    "@types/react-router-dom": "^4.2.0",
    "@types/react-test-renderer": "^16.0.0",
    "@types/webdriverio": "^5.0.0",
    "@types/webpack-env": "^1.16.0",
    "awesome-typescript-loader": "^5.2.1",
    "copy-webpack-plugin": "^5.1.1",
    "cross-env": "^7.0.3",
    "css-loader": "^2.1.0",
    "dateformat": "^4.5.1",
    "electron": "13.6.6",
    "electron-builder": "^22.14.3",
    "electron-devtools-installer": "3.2.0",
    "electron-mock-ipc": "0.3.9",
    "file-loader": "^3.0.1",
    "html-webpack-plugin": "^3.2.0",
    "husky": "^1.3.1",
    "image-webpack-loader": "^4.6.0",
    "jest": "^27.0.5",
    "node-sass": "^7.0.0",
    "pagination-calculator": "^1.0.0",
    "prettier": "^2.0.5",
    "react-hot-loader": "^4.6.3",
    "react-test-renderer": "^16.2.0",
    "sass-loader": "^7.3.1",
    "source-map-loader": "^0.2.4",
    "spectron": "^15.0.0",
    "style-loader": "^0.23.1",
    "ts-jest": "^27.0.3",
    "tslint": "^5.15.0",
    "tslint-config-airbnb": "^5.4.2",
    "tslint-config-prettier": "^1.18.0",
    "tslint-react": "^4.0.0",
    "typescript": "^3.8.3",
    "webpack": "^4.29.0",
    "webpack-cli": "^3.3.11",
    "webpack-dev-server": "^3.11.0",
    "webpack-merge": "^4.2.1"
  },
  "dependencies": {
    "@electron/remote": "^1.2.0",
    "animate.css": "^3.7.2",
    "electron-is-accelerator": "^0.2.0",
    "electron-log": "^4.3.5",
    "electron-settings": "^3.2.0",
    "fix-path": "^3.0.0",
    "history": "^4.10.1",
    "lodash": "^4.17.21",
    "material-design-icons": "^3.0.1",
    "materialize-css": "^1.0.0",
    "promise-timeout": "^1.3.0",
    "react": "^16.13.1",
    "react-animated-css": "^1.2.1",
    "react-dom": "^16.13.1",
    "react-keyboard-event-handler": "^1.5.4",
    "react-materialize": "^2.6.0",
    "react-router": "^5.1.2",
    "react-router-dom": "^5.1.2",
    "react-treebeard": "^3.2.4",
    "string-replace-to-array": "^1.0.3"
  }
}


================================================
FILE: src/main/AppUtilities.ts
================================================
import * as electronSettings from 'electron-settings'
import { DEFAULT_SYSTEM_SETTINGS, DEFAULT_USER_SETTINGS, SystemSettings, UserSettings } from '../shared/settings'

export const installExtensions = async () => {
    const installer = require('electron-devtools-installer')
    const forceDownload = !!process.env.UPGRADE_EXTENSIONS
    const extensions = ['REACT_DEVELOPER_TOOLS']

    return Promise.all(extensions.map(name => installer.default(installer[name], forceDownload))).catch(console.info)
}

export const getSystemSettings = (): SystemSettings => {
    return (electronSettings.get('system_settings') as any) || DEFAULT_SYSTEM_SETTINGS
}

export const getUserSettings = (): UserSettings => {
    return (electronSettings.get('user_settings') as any) || DEFAULT_USER_SETTINGS
}


================================================
FILE: src/main/AppWindows.ts
================================================
import { BrowserWindow, Menu, app, nativeTheme } from 'electron'
import * as url from 'url'
import * as path from 'path'

export const createMainWindow = (): BrowserWindow => {
    nativeTheme.themeSource = 'light'
    const mainWindow = new BrowserWindow({
        width: 1000,
        height: 600,
        center: true,
        title: 'Gopass UI',
        icon: path.join(__dirname, 'assets', 'icon.png'),
        webPreferences: {
            enableRemoteModule: true,
            nodeIntegration: true,
            contextIsolation: false,
            worldSafeExecuteJavaScript: false
        }
    })

    if (process.env.NODE_ENV !== 'production') {
        mainWindow.loadURL('http://localhost:2003')

        mainWindow.webContents.openDevTools()
    } else {
        mainWindow.loadURL(
            url.format({
                pathname: path.join(__dirname, 'explorer', 'index.html'),
                protocol: 'file:',
                slashes: true
            })
        )
    }

    return mainWindow
}

export const createSearchWindow = (show: boolean): BrowserWindow => {
    const searchWindow = new BrowserWindow({
        show,
        width: process.env.NODE_ENV !== 'production' ? 1200 : 600,
        height: 600,
        frame: false,
        center: true,
        skipTaskbar: true,
        title: 'Gopass UI Search Window',
        resizable: false,
        webPreferences: {
            enableRemoteModule: true,
            nodeIntegration: true,
            contextIsolation: false,
            worldSafeExecuteJavaScript: false
        }
    })

    searchWindow.setMenu(null)

    if (process.env.NODE_ENV !== 'production') {
        searchWindow.loadURL('http://localhost:2004')

        searchWindow.webContents.openDevTools()
    } else {
        searchWindow.loadURL(
            url.format({
                pathname: path.join(__dirname, 'search', 'index.html'),
                protocol: 'file:',
                slashes: true
            })
        )
    }

    return searchWindow
}

export const hideMainWindow = (mainWindow: BrowserWindow | null) => {
    if (mainWindow) {
        if (app.hide) {
            // Linux and MacOS
            app.hide()
        } else {
            // for Windows
            mainWindow.blur()
            mainWindow.hide()
        }
    }
}

export const buildContextMenu = (mainWindow: BrowserWindow | null, searchWindow: BrowserWindow | null) =>
    Menu.buildFromTemplate([
        {
            label: 'Explorer',
            click: () => {
                if (mainWindow) {
                    mainWindow.show()
                } else {
                    mainWindow = createMainWindow()
                }
            }
        },
        {
            label: 'Search',
            click: () => {
                if (searchWindow) {
                    searchWindow.show()
                } else {
                    searchWindow = createSearchWindow(true)
                }
            }
        },
        {
            type: 'separator'
        },
        {
            label: 'Quit',
            click: () => {
                app.quit()
            }
        }
    ])


================================================
FILE: src/main/GopassExecutor.ts
================================================
import { exec } from 'child_process'
import { IpcMainEvent } from 'electron'

export interface GopassOptions {
    executionId: string
    command: string
    pipeTextInto?: string
    args?: string[]
}

export default class GopassExecutor {
    public static async handleEvent(event: IpcMainEvent, options: GopassOptions) {
        const argsString = options.args ? ` ${options.args.join(' ')}` : ''
        const pipeText = options.pipeTextInto ? `echo "${options.pipeTextInto}" | ` : ''
        const command = `${pipeText}gopass ${options.command}${argsString}`

        exec(command, (err: Error | null, stdout: string, stderr: string) => {
            event.sender.send(`gopass-answer-${options.executionId}`, {
                status: err ? 'ERROR' : 'OK',
                executionId: options.executionId,
                payload: err ? stderr : stdout
            })
        })
    }
}


================================================
FILE: src/main/index.ts
================================================
import { app, BrowserWindow, Event, globalShortcut, ipcMain, IpcMainEvent, Tray, session, shell, Accelerator } from 'electron'
import { URL } from 'url'
import * as path from 'path'
import * as fixPath from 'fix-path'
import * as electronSettings from 'electron-settings'

import { SystemSettings, UserSettings } from '../shared/settings'
import GopassExecutor from './GopassExecutor'
import { buildContextMenu, createMainWindow, createSearchWindow } from './AppWindows'
import { getSystemSettings, getUserSettings, installExtensions } from './AppUtilities'

const isDevMode = process.env.NODE_ENV !== 'production'
fixPath()

import * as remoteMain from '@electron/remote/main'
remoteMain.initialize()

let mainWindow: BrowserWindow | null
let searchWindow: BrowserWindow | null
let tray: Tray

const setGlobalSearchWindowShortcut = (shortcut: Accelerator, previousShortcut?: Accelerator) => {
    // unregister previously used shortcut if Electron recognises it as valid
    if (previousShortcut) {
        try {
            globalShortcut.unregister(previousShortcut)
        } catch (e) {
            // previous shortcut was not considered as valid
        }
    }

    // unregister shortcut from other usages within the application
    // in case an error is thrown, Electron does not recognise it as valid and the method returns
    try {
        globalShortcut.unregister(shortcut)
    } catch (e) {
        return
    }

    // register shortcut once sure it is a valid and usage-free Accelerator for Electron
    globalShortcut.register(shortcut, () => {
        if (searchWindow) {
            if (searchWindow.isFocused()) {
                searchWindow.hide()
            } else {
                searchWindow.show()
            }
        } else {
            searchWindow = createSearchWindow(true)
        }
    })
}

const setTray = (showTray: boolean) => {
    if (showTray) {
        if (!tray || tray.isDestroyed()) {
            if (process.platform === 'darwin') {
                tray = new Tray(path.join(__dirname, 'assets', 'icon-mac@2x.png'))
            } else if (process.platform === 'linux') {
                tray = new Tray(path.join(__dirname, 'assets', 'icon@2x.png'))
            } else {
                tray = new Tray(path.join(__dirname, 'assets', 'icon.png'))
            }

            tray.setToolTip('Gopass UI')
            tray.setContextMenu(buildContextMenu(mainWindow, searchWindow))
        }
    } else {
        if (tray && !tray.isDestroyed()) {
            tray.destroy()
        }
    }
}

const listenToIpcEvents = () => {
    ipcMain.on('gopass', GopassExecutor.handleEvent)

    ipcMain.on('getUserSettings', (event: IpcMainEvent) => {
        event.returnValue = getUserSettings()
    })

    ipcMain.on('hideSearchWindow', () => {
        if (searchWindow) {
            searchWindow.hide()
        }
    })

    ipcMain.on('getSystemSettings', (event: IpcMainEvent) => {
        event.returnValue = getSystemSettings()
    })

    ipcMain.on('updateUserSettings', (_: Event, update: Partial<UserSettings>) => {
        const current = getUserSettings()
        const all = { ...current, ...update }

        // modify aspects of application where updated settings need take effect
        if (update.searchShortcut && update.searchShortcut !== current.searchShortcut) {
            setGlobalSearchWindowShortcut(update.searchShortcut, current.searchShortcut)
        }
        if (update.showTray && update.showTray !== current.showTray) {
            setTray(update.showTray)
        }
        if (update.startOnLogin && update.startOnLogin !== current.startOnLogin) {
            configureStartOnLogin(update.startOnLogin)
        }

        electronSettings.set('user_settings', all as any)
    })

    ipcMain.on('updateSystemSettings', (_: Event, update: Partial<SystemSettings>) => {
        electronSettings.set('system_settings', { ...getSystemSettings(), ...update } as any)
    })
}

const configureStartOnLogin = (startOnLogin: boolean) => {
    app.setLoginItemSettings({
        openAtLogin: startOnLogin,
        openAsHidden: true
    })
}

const setup = async () => {
    /**
     * Adds a restrictive default CSP for all fetch directives to all HTTP responses of the web server.
     * About default-src: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/default-src
     * Electron reference: https://electronjs.org/docs/tutorial/security#6-define-a-content-security-policy
     */
    if (session.defaultSession && !isDevMode) {
        session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
            callback({
                responseHeaders: {
                    ...details.responseHeaders,
                    'Content-Security-Policy': [`default-src 'self' 'unsafe-inline'; connect-src https://api.github.com`]
                }
            })
        })
    }

    if (isDevMode) {
        await installExtensions()
    }

    const settings = getUserSettings()

    mainWindow = createMainWindow()

    mainWindow.on('closed', () => {
        app.quit()
    })

    searchWindow = createSearchWindow(false)

    setGlobalSearchWindowShortcut(settings.searchShortcut, undefined)
    setTray(settings.showTray)
    configureStartOnLogin(settings.startOnLogin)

    listenToIpcEvents()
}

app.on('ready', setup)

app.on('window-all-closed', () => {
    if (process.platform !== 'darwin') {
        app.quit()
    }
})

app.on('activate', () => {
    if (mainWindow === null) {
        mainWindow = createMainWindow()
    }
    if (searchWindow === null) {
        searchWindow = createSearchWindow(false)
    }
})

/**
 * Prevent navigation to every target that lays outside of the Electron application (localhost:2003 and localhost:2004)
 * Reference: https://electronjs.org/docs/tutorial/security#12-disable-or-limit-navigation
 */
app.on('web-contents-created', (event, contents) => {
    contents.on('will-navigate', (navigationEvent, navigationUrl) => {
        const parsedUrl = new URL(navigationUrl)

        if (!parsedUrl.origin.startsWith('http://localhost:200')) {
            navigationEvent.preventDefault()
        }
    })
})

/**
 * Prevents unwanted modules from 'remote' from being used.
 * Reference: https://electronjs.org/docs/tutorial/security#16-filter-the-remote-module
 */
const allowedRemoteModules = new Set(['app'])
app.on('remote-get-builtin', (event, webContents, moduleName) => {
    if (!allowedRemoteModules.has(moduleName)) {
        event.preventDefault()
        console.warn(`Blocked module "${moduleName}"`)
    }
})

const allowedModules = new Set()
const proxiedModules = new Map()
app.on('remote-require', (event, webContents, moduleName) => {
    if (proxiedModules.has(moduleName)) {
        const proxiedModule = proxiedModules.get(moduleName)
        event.returnValue = proxiedModule
        console.warn(`Proxied remote-require of module "${moduleName}" to "${proxiedModule}"`)
    }
    if (!allowedModules.has(moduleName)) {
        event.preventDefault()
        console.warn(`Blocked remote-require of module "${moduleName}"`)
    }
})

const allowedGlobals = new Set()
app.on('remote-get-global', (event, webContents, globalName) => {
    if (!allowedGlobals.has(globalName)) {
        event.preventDefault()
    }
})

app.on('remote-get-current-window', event => {
    event.preventDefault()
})

app.on('remote-get-current-web-contents', event => {
    event.preventDefault()
})


================================================
FILE: src/renderer/common/Settings.ts
================================================
import { ipcRenderer } from 'electron'
import { SystemSettings, UserSettings } from '../../shared/settings'
import set = Reflect.set

export class Settings {
    public static getUserSettings(): UserSettings {
        return ipcRenderer.sendSync('getUserSettings')
    }

    public static updateUserSettings(settings: Partial<UserSettings>) {
        ipcRenderer.send('updateUserSettings', settings)
    }

    public static getSystemSettings(): SystemSettings {
        return ipcRenderer.sendSync('getSystemSettings')
    }

    public static updateSystemSettings(settings: Partial<SystemSettings>) {
        ipcRenderer.send('updateSystemSettings', settings)
    }
}


================================================
FILE: src/renderer/common/notifications/Notification.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'
import { Animated } from 'react-animated-css'

import { useNotificationContext } from './NotificationProvider'

const DEFAULT_TIMEOUT = 3000

export default function NotificationView(props: { dismissTimeout?: number }) {
    const { notification, hide, isHidden } = useNotificationContext()
    const { dismissTimeout = DEFAULT_TIMEOUT } = props

    React.useEffect(() => {
        let timeoutId = 0

        if (notification && dismissTimeout !== 0) {
            timeoutId = window.setTimeout(() => hide(), dismissTimeout)
        }

        return () => {
            if (timeoutId !== 0) {
                clearTimeout(timeoutId)
            }
        }
    }, [notification, dismissTimeout])

    return (
        <Animated animationIn='fadeInDown' animationInDuration={1000} animationOutDuration={1000} animationOut='fadeOutUp' isVisible={!isHidden}>
            <m.Row>
                <m.Col s={12}>
                    {notification && (
                        <m.CardPanel className={`${notification.status === 'OK' ? 'green' : 'red'} lighten-1 black-text`}>
                            <m.Row style={{ marginBottom: 0 }}>
                                <m.Col s={11}>
                                    <span>{notification ? notification.message : ''}</span>
                                </m.Col>
                                <m.Col s={1} style={{ textAlign: 'right' }}>
                                    <a className='black-text link' onClick={() => hide()}>
                                        <m.Icon small>close</m.Icon>
                                    </a>
                                </m.Col>
                            </m.Row>
                        </m.CardPanel>
                    )}
                </m.Col>
            </m.Row>
        </Animated>
    )
}


================================================
FILE: src/renderer/common/notifications/NotificationProvider.tsx
================================================
import * as React from 'react'

export interface Notification {
    status: 'OK' | 'ERROR'
    message: string
}

export interface NotificationContext {
    notification?: Notification
    isHidden: boolean
    show: (notification: Notification) => void
    hide: () => void
}

const Context = React.createContext<NotificationContext | null>(null)

export function useNotificationContext() {
    const context = React.useContext(Context)

    if (!context) {
        throw Error('NO Context!')
    }

    return context
}

export default function NotificationProvider({ children }: any) {
    const [notification, setNotification] = React.useState<Notification | undefined>()
    const [isHidden, setIsHidden] = React.useState<boolean>(false)

    return (
        <Context.Provider
            value={{
                notification,
                isHidden,
                show: newNotification => {
                    setNotification(newNotification)
                    setIsHidden(false)
                },
                hide: () => {
                    setIsHidden(true)
                    setTimeout(() => {
                        setNotification(undefined)
                    }, 1000)
                }
            }}
        >
            {children}
        </Context.Provider>
    )
}


================================================
FILE: src/renderer/components/ExternalLink.tsx
================================================
import * as React from 'react'
import { shell } from 'electron'

export function ExternalLink(props: { url: string; children: any }) {
    const { url, children } = props

    return (
        <a className='link' onClick={() => shell.openExternal(url)}>
            {children}
        </a>
    )
}


================================================
FILE: src/renderer/components/GoBackNavigationButton.tsx
================================================
import * as React from 'react'
import { withRouter } from 'react-router'
import { History } from 'history'

import { RoundActionButton } from './RoundActionButton'

const GoBackNavigationButton = (props: { history: History }) => (
    <div style={{ paddingTop: '0.75rem' }}>
        <RoundActionButton icon='arrow_back' onClick={() => props.history.replace('/')} />
    </div>
)

export default withRouter(GoBackNavigationButton)


================================================
FILE: src/renderer/components/PaginatedTable.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'
import { paginationCalculator } from 'pagination-calculator'
import { PageInformation } from 'pagination-calculator/dist/paginationCalculator'

export interface TableColumn {
    fieldName: string
    label: string
}

export interface TableRow {
    id: string

    [fieldName: string]: string | React.ReactNode
}

export interface PaginatedTableProps {
    columns: TableColumn[]
    rows: TableRow[]
}

export interface PaginatedTableState {
    page: number
    pageSize: number
}

export default class PaginatedTable extends React.Component<PaginatedTableProps, PaginatedTableState> {
    constructor(props: PaginatedTableProps) {
        super(props)
        this.state = {
            page: 1,
            pageSize: 7 // TODO: make configurable through settings page
        }
    }

    public render() {
        const { columns, rows } = this.props
        const pagination = paginationCalculator({
            total: rows.length,
            current: this.state.page,
            pageSize: this.state.pageSize,
            pageLimit: Math.ceil(rows.length / this.state.pageSize)
        })
        const pageRows = rows.slice(pagination.showingStart - 1, pagination.showingEnd)

        return (
            <>
                <m.Table>
                    <thead>
                        <tr>
                            {columns.map(column => (
                                <th key={column.fieldName} data-field={column.fieldName}>
                                    {column.label}
                                </th>
                            ))}
                        </tr>
                    </thead>

                    <tbody>
                        {pageRows.map(row => (
                            <tr key={row.id}>
                                {columns.map(column => (
                                    <td key={`${row.id}-${column.fieldName}`}>{row[column.fieldName]}</td>
                                ))}
                            </tr>
                        ))}
                    </tbody>
                </m.Table>

                {this.renderPagination(pagination)}
            </>
        )
    }

    private renderPagination(pagination: PageInformation) {
        return (
            <m.Pagination
                items={pagination.pageCount}
                activePage={pagination.current}
                maxButtons={pagination.pageCount <= 8 ? pagination.pageCount : 8}
                onSelect={this.changeToPage(pagination.pageCount)}
            />
        )
    }

    private changeToPage = (maxPageNumber: number) => {
        return (page: number) => {
            if (page <= maxPageNumber && page >= 1) {
                this.setState({ page })
            }
        }
    }
}


================================================
FILE: src/renderer/components/RoundActionButton.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'

export interface RoundActionBtnProps {
    icon: string
    onClick?: () => void
}

export const RoundActionButton = ({ icon, onClick }: RoundActionBtnProps) => (
    <m.Button floating large className='red' waves='light' icon={icon} onClick={onClick} style={{ marginRight: '0.75rem' }} />
)


================================================
FILE: src/renderer/components/loading-screen/LoadingScreen.css
================================================
.loading-screen-wrapper {
    display: flex;
    align-items: center;
    justify-content: center;
    height: 500px;
    font-size: 24px;
}

.loading-screen-message {
    width: 40%;
    min-width: 300px;
    text-align: center;
}


================================================
FILE: src/renderer/components/loading-screen/LoadingScreen.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'

import './LoadingScreen.css'

const WAITING_TEXTS = [
    'Loading, please wait...',
    'Still doing something...',
    'It seems to takes some time...',
    'Doop di doop di douuu...',
    "I'm sorry that it takes longer.",
    'Maybe there is a persistent problem. Sorry for that!'
]

export function LoadingScreen() {
    const [waitingTextIndex, setWaitingTextIndex] = React.useState(0)

    React.useEffect(() => {
        const timeout = setInterval(() => {
            setWaitingTextIndex(waitingTextIndex + 1)
        }, 2000)

        return () => clearInterval(timeout)
    }, [])

    return (
        <div className='loading-screen-wrapper'>
            <div className='loading-screen-message'>
                <p>{WAITING_TEXTS[waitingTextIndex % WAITING_TEXTS.length]}</p>
                <m.ProgressBar />
            </div>
        </div>
    )
}


================================================
FILE: src/renderer/components/tree/TreeComponent.tsx
================================================
import * as React from 'react'
import * as t from 'react-treebeard'
import { globalStyle } from './TreeStyle'
import { TreeHeader as Header } from './TreeHeader'

export interface Tree {
    name: string
    toggled?: boolean
    loading?: boolean
    children?: Tree[]
    path: string
}

export interface TreeComponentProps {
    tree: Tree
    onLeafClick: (leafId: string) => void
}

interface TreeComponentState {
    selectedNode?: any
}
export default class TreeComponent extends React.Component<TreeComponentProps, TreeComponentState> {
    public state: TreeComponentState = {}

    public render() {
        return <t.Treebeard data={this.props.tree} decorators={{ ...t.decorators, Header }} onToggle={this.onToggle} style={globalStyle} />
    }

    private onToggle = (node: any, toggled: boolean) => {
        // if no children (thus being a leaf and thereby an entry), trigger the handler
        if (!node.children || node.children.length === 0) {
            this.props.onLeafClick(node.path)
        }

        // previously selected node is no more active
        if (this.state.selectedNode) {
            this.state.selectedNode.active = false
        }

        // newly selected node shall be active
        node.active = true

        // ...and toggled if having children
        if (node.children) {
            node.toggled = toggled
        }
        this.setState({ selectedNode: node })

        if (node.children && node.children.length === 1) {
            this.onToggle(node.children[0], true)
        }
    }
}


================================================
FILE: src/renderer/components/tree/TreeHeader.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'

import { deriveIconFromSecretName } from '../../secrets/deriveIconFromSecretName'

export const TreeHeader = ({ style, node }: any) => {
    let iconType = 'folder'

    const isLeaf = !node.children && node.path
    if (!node.children && node.path) {
        iconType = deriveIconFromSecretName(node.name)
    }
    return (
        <div style={style.base} className='icon-wrapper'>
            {node.children && (
                <div className={`chevron ${node.toggled && 'toggled'}`}>
                    <m.Icon small>chevron_right</m.Icon>
                </div>
            )}
            <m.Icon small>{iconType}</m.Icon>

            {node.name}
        </div>
    )
}


================================================
FILE: src/renderer/components/tree/TreeStyle.ts
================================================
const white = '#FFFFFF'
const none = 'none'

export const globalStyle = {
    tree: {
        base: {
            listStyle: none,
            backgroundColor: white,
            margin: 0,
            padding: 0,
            color: '#000000',
            fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif;',
            fontSize: '18px'
        },
        node: {
            base: {
                position: 'relative',
                borderLeft: '1px solid #ececec',
                padding: '4px 12px 4px',
                marginLeft: '8px'
            },
            link: {
                cursor: 'pointer',
                position: 'relative',
                display: 'block'
            },
            activeLink: {
                background: white
            },
            toggle: {
                base: {
                    display: none
                },
                wrapper: {
                    position: 'absolute',
                    top: '50%',
                    left: '50%',
                    margin: '-7px 0 0 -7px',
                    height: '14px'
                },
                height: 0,
                width: 0,
                arrow: {
                    fill: white,
                    strokeWidth: 0
                }
            },
            header: {
                base: {
                    display: 'inline-block',
                    verticalAlign: 'top',
                    color: '#555',
                    cursor: 'pointer'
                }
            },
            subtree: {
                listStyle: none,
                paddingLeft: '60px'
            },
            loading: {
                color: '#E2C089'
            }
        }
    }
}


================================================
FILE: src/renderer/explorer-app/ExplorerApplication.css
================================================
.secret-explorer {
    position: fixed;
    z-index: 10;
    top: 0;
    left: 0;
    bottom: 0;
    width: 450px;
    overflow: auto;
    min-width: 300px;
    border-right: 1px solid rgba(0, 0, 0, 0.14);
}

.secret-explorer .search-bar {
    background: #f9f9f9;
    border-bottom: 1px solid rgba(0, 0, 0, 0.14);
    position: sticky;
    top: 0;
    z-index: 10;
}

.secret-explorer .search-bar * {
    margin: 0 !important;
    border: none !important;
}

.secret-explorer .search-bar > div {
    padding: 0 !important;
}

.secret-explorer .search-bar input {
    box-sizing: border-box !important;
    padding: 24px !important;
}

.secret-explorer > ul > li {
    border: 0;
}

.secret-explorer .chevron {
    display: inline-block;
    user-select: none;
}

.secret-explorer .chevron.toggled .material-icons {
    transform: rotate(90deg);
}

.secret-explorer .icon-wrapper > .material-icons:only-child {
    margin-left: 30px;
}

.secret-explorer .chevron .material-icons {
    margin: 0;
}

.secret-explorer i.material-icons {
    position: relative;
    top: 7px;
    margin-right: 8px;
}

.main-content {
    resize: both;
    padding-left: 450px;
}

.m-top {
    margin-top: 55px;
}

.panel-headline {
    margin-top: 0;
}

.link {
    cursor: pointer;
}

span.code {
    font-family: Consolas, monospace;
}

.card-panel pre {
    margin-bottom: 0px;
}

.password-strength-sum {
    height: 100px;
    width: 100px;
    align-items: center;
    justify-content: center;
    text-align: center;
    border-radius: 50%;
    display: flex;
    font-size: 42px;
    font-weight: 200;
}

.password-strength-sum.red {
    color: white;
}


================================================
FILE: src/renderer/explorer-app/ExplorerApplication.tsx
================================================
import * as React from 'react'

import SecretExplorer from './side-navigation/SecretExplorer'
import MainContent from './MainContent'

import './ExplorerApplication.css'

const ExplorerApplication = () => {
    return (
        <>
            <SecretExplorer />
            <MainContent />
        </>
    )
}

export default ExplorerApplication


================================================
FILE: src/renderer/explorer-app/GithubService.ts
================================================
export interface GithubTag {
    url: string
    ref: string
}

export default class GithubService {
    public static getTagsOfRepository(owner: string, repositoryName: string): Promise<GithubTag[]> {
        return new Promise((resolve, reject) => {
            const httpRequest = new XMLHttpRequest()
            const url = `https://api.github.com/repos/${owner}/${repositoryName}/git/refs/tags`

            httpRequest.open('GET', url)
            httpRequest.onload = e => {
                if (httpRequest.status >= 200 && httpRequest.status < 300) {
                    resolve(JSON.parse(httpRequest.response) as GithubTag[])
                } else {
                    reject({
                        status: httpRequest.status,
                        statusText: httpRequest.statusText
                    })
                }
            }
            httpRequest.send()
        })
    }
}


================================================
FILE: src/renderer/explorer-app/MainContent.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'
import { match, Route } from 'react-router-dom'

import SettingsPage from './pages/SettingsPage'
import HomePage from './pages/HomePage'
import MainNavigation from './components/MainNavigation'
import GoBackNavigation from '../components/GoBackNavigationButton'
import Notification from '../common/notifications/Notification'
import NotificationProvider from '../common/notifications/NotificationProvider'
import PasswordHealthOverview from './pages/PasswordHealthPage'
import AddSecretPage from './pages/AddSecretPage'
import SecretDetailsPage from './pages/details/SecretDetailsPage'
import MountsPage from './pages/MountsPage'
import AddMountPage from './pages/AddMountPage'

const Routes = () => (
    <>
        <Route
            path='/'
            exact
            render={() => (
                <>
                    <MainNavigation />
                    <HomePage />
                </>
            )}
        />
        <Route
            path='/secret/:encodedSecretName'
            component={(props: { match: match<{ encodedSecretName: string }>; location: { search?: string } }) => {
                const secretName = atob(props.match.params.encodedSecretName)
                const isAdded = props.location.search ? props.location.search === '?added' : false

                return (
                    <>
                        <MainNavigation />
                        <SecretDetailsPage secretName={secretName} isAdded={isAdded} />
                    </>
                )
            }}
        />
        <Route
            path='/settings'
            exact
            render={() => (
                <>
                    <GoBackNavigation />
                    <SettingsPage />
                </>
            )}
        />
        <Route path='/mounts' exact render={() => <MountsPage />} />
        <Route
            path='/password-health'
            exact
            render={() => (
                <>
                    <GoBackNavigation />
                    <PasswordHealthOverview />
                </>
            )}
        />
        <Route path='/add-mount' exact render={() => <AddMountPage />} />
        <Route
            path='/add-secret'
            exact
            render={() => (
                <>
                    <GoBackNavigation />
                    <AddSecretPage />
                </>
            )}
        />
    </>
)

const MainContent = () => (
    <div className='main-content'>
        <NotificationProvider>
            <Notification />
            <m.Row>
                <m.Col s={12}>
                    <Routes />
                </m.Col>
            </m.Row>
        </NotificationProvider>
    </div>
)

export default MainContent


================================================
FILE: src/renderer/explorer-app/SecretsProvider.tsx
================================================
import * as React from 'react'
import { Tree } from '../components/tree/TreeComponent'
import SecretsFilterService from './side-navigation/SecretsFilterService'
import SecretsDirectoryService from './side-navigation/SecretsDirectoryService'
import Gopass from '../secrets/Gopass'

export interface SecretsContext {
    tree: Tree
    searchValue: string

    applySearchToTree: (searchValue: string) => void
    reloadSecretNames: () => Promise<void>
}

const Context = React.createContext<SecretsContext | undefined>(undefined)
export const useSecretsContext = () => {
    const context = React.useContext(Context)
    if (!context) {
        throw Error('no secrets context!')
    }

    return context
}

export const SecretsProvider = ({ children }: { children: React.ReactNode }) => {
    const [tree, setTree] = React.useState<Tree>({ name: 'Stores', toggled: true, children: [], path: '' })
    const [searchValue, setSearchValue] = React.useState<string>('')
    const [secretNames, setSecretNames] = React.useState<string[]>([])

    const applySearchToTree = (newSearchValue?: string, updatedSecretNames?: string[]) => {
        let searchValueToUse = searchValue
        if (newSearchValue !== undefined) {
            setSearchValue(newSearchValue)
            searchValueToUse = newSearchValue
        }
        const filteredSecretNames = SecretsFilterService.filterBySearch(updatedSecretNames || secretNames, searchValueToUse)
        const newTree: Tree = SecretsDirectoryService.secretPathsToTree(filteredSecretNames, tree, filteredSecretNames.length <= 15)

        setTree(newTree)
    }
    const loadSecretsAndBuildTree = async (newSearchValue: string | undefined = undefined) => {
        const allSecretNames = await Gopass.getAllSecretNames()
        setSecretNames(allSecretNames)
        applySearchToTree(newSearchValue !== undefined ? newSearchValue : searchValue, allSecretNames)
    }

    const providerValue: SecretsContext = {
        tree,
        searchValue,
        reloadSecretNames: () => loadSecretsAndBuildTree(),
        applySearchToTree
    }

    return <Context.Provider value={providerValue}>{children}</Context.Provider>
}


================================================
FILE: src/renderer/explorer-app/components/EnvironmentTest.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'
import { timeout } from 'promise-timeout'
import { shell } from 'electron'

import Gopass from '../../secrets/Gopass'
import { ExternalLink } from '../../components/ExternalLink'
import { Settings } from '../../common/Settings'

type ErrorDetails = 'GOPASS_CONNECTION' | 'DECRYPTION' | undefined

export function EnvironmentTest() {
    const [environmentTestStatus, setEnvironmentTestStatus] = React.useState<'PENDING' | 'RUNNING' | 'OK' | 'ERROR'>('PENDING')
    const [errorDetails, setErrorDetails] = React.useState<ErrorDetails>()

    function reset() {
        setEnvironmentTestStatus('PENDING')
        setErrorDetails(undefined)
    }

    function executeTest() {
        setEnvironmentTestStatus('RUNNING')

        timeout(Gopass.getAllSecretNames(), 1500)
            .then(([firstEntry]) => {
                if (firstEntry) {
                    timeout(Gopass.show(firstEntry), 10000)
                        .then(() => {
                            setEnvironmentTestStatus('OK')
                            Settings.updateSystemSettings({ environmentTestSuccessful: true })
                        })
                        .catch(() => {
                            setEnvironmentTestStatus('ERROR')
                            Settings.updateSystemSettings({ environmentTestSuccessful: false })
                            setErrorDetails('DECRYPTION')
                        })
                }
            })
            .catch(() => {
                setEnvironmentTestStatus('ERROR')
                Settings.updateSystemSettings({ environmentTestSuccessful: false })
                setErrorDetails('GOPASS_CONNECTION')
            })
    }

    switch (environmentTestStatus) {
        case 'PENDING':
            return <PendingContent executeTest={executeTest} />
        case 'RUNNING':
            return <RunningContent />
        case 'OK':
            return <OkContent />
        default:
        case 'ERROR':
            return <ErrorContent errorDetails={errorDetails} reset={reset} />
    }
}

function PendingContent(props: { executeTest: () => void }) {
    return (
        <>
            Your system has to meet the following requirements for Gopass UI to work properly:
            <ol>
                <li>Gopass needs to be installed and configured to be up and running 🙂</li>
                <li>
                    MacOS: you should use <span className='code'>pinentry-mac</span> as a GPG passphrase dialog tool (available{' '}
                    <ExternalLink url='https://formulae.brew.sh/formula/pinentry-mac'>as Brew formulae</ExternalLink>)
                </li>
            </ol>
            <p>During the environment test you might be asked for your GPG passphrase. Please unlock your GPG keypair by entering it.</p>
            <m.Button onClick={props.executeTest} waves='light'>
                Test your environment
            </m.Button>
        </>
    )
}

function RunningContent() {
    return (
        <div style={{ textAlign: 'center' }}>
            <m.Preloader size='small' />
            <br />
            <br />
            <strong>Tests are running...</strong>
        </div>
    )
}

function ErrorContent(props: { errorDetails: ErrorDetails; reset: () => void }) {
    return (
        <div style={{ textAlign: 'center' }}>
            <m.Icon large>error</m.Icon>
            <br />
            <h4>Oops, something went wrong.</h4>
            {props.errorDetails && (
                <>
                    <strong>
                        {props.errorDetails === 'DECRYPTION' && <>It wasn't possible to decrypt your secrets.</>}
                        {props.errorDetails === 'GOPASS_CONNECTION' && <>It wasn't possible to access the gopass CLI.</>}
                    </strong>
                    <br />
                    <br />
                </>
            )}
            Do you need help getting started?{' '}
            <a onClick={() => shell.openExternal('https://github.com/codecentric/gopass-ui/issues')}>Please create an issue.</a>
            <br />
            <br />
            <m.Button onClick={props.reset} waves='light'>
                restart
            </m.Button>
        </div>
    )
}

function OkContent() {
    return (
        <div style={{ textAlign: 'center' }}>
            <m.Icon large>done</m.Icon>
            <br />
            <strong>Everything looks fine</strong>
        </div>
    )
}


================================================
FILE: src/renderer/explorer-app/components/LastVersionInfo.tsx
================================================
import * as React from 'react'
import { app } from '@electron/remote'
import GithubService, { GithubTag } from '../GithubService'
import { ExternalLink } from '../../components/ExternalLink'
import { Settings } from '../../common/Settings'

const ONE_HOUR_IN_MILLIS = 3600000
const VERSION_CHECK_INTERVAL = ONE_HOUR_IN_MILLIS

export const LatestVersionInfo = () => {
    const { releaseCheck } = Settings.getSystemSettings()
    const [tags, setTags] = React.useState<GithubTag[]>([])

    React.useEffect(() => {
        const millisNow = new Date().getTime()
        const shouldFetchTags = !releaseCheck || !releaseCheck.lastCheckTimestamp || millisNow - VERSION_CHECK_INTERVAL > releaseCheck.lastCheckTimestamp

        if (shouldFetchTags) {
            GithubService.getTagsOfRepository('codecentric', 'gopass-ui').then(newTags => {
                setTags(newTags)
                Settings.updateSystemSettings({ releaseCheck: { lastCheckTimestamp: millisNow, results: newTags } })
            })
        } else {
            setTags(releaseCheck.results)
        }
    }, [])

    const lastTag = tags[tags.length - 1]
    const lastTagName = lastTag ? lastTag.ref.slice(10, lastTag.ref.length) : ''
    const appVersion = app.getVersion()

    if (lastTagName) {
        return lastTagName.includes(appVersion) ? (
            <>
                You have the latest version <strong>{appVersion}</strong> of Gopass UI installed 🎉
            </>
        ) : (
            <>
                Your Gopass UI version ({appVersion}) is out of date 😕 Make sure you got the latest release of Gopass UI:&nbsp;
                <ExternalLink url='https://github.com/codecentric/gopass-ui/releases/latest'>{`${lastTagName} on Github`}</ExternalLink>
            </>
        )
    }

    return null
}


================================================
FILE: src/renderer/explorer-app/components/MainNavigation.tsx
================================================
import * as React from 'react'
import { History } from 'history'
import { withRouter } from 'react-router'

import { RoundActionButton } from '../../components/RoundActionButton'
import { useNotificationContext } from '../../common/notifications/NotificationProvider'
import Gopass from '../../secrets/Gopass'
import { useSecretsContext } from '../SecretsProvider'

interface MainNavigationViewProps {
    history: History
}

function MainNavigationComponent({ history }: MainNavigationViewProps) {
    const notificationContext = useNotificationContext()
    const secretsContext = useSecretsContext()

    const refreshGopassStores = async () => {
        try {
            await Gopass.sync()
            await secretsContext.reloadSecretNames()
            notificationContext.show({ status: 'OK', message: 'Your stores have been synchronised successfully.' })
        } catch (err) {
            notificationContext.show({ status: 'ERROR', message: `Oops, something went wrong: ${JSON.stringify(err)}` })
        }
    }

    return (
        <div style={{ paddingTop: '0.75rem' }}>
            <RoundActionButton icon='home' onClick={() => history.replace('/')} />
            <RoundActionButton icon='add' onClick={() => history.replace('/add-secret')} />
            <RoundActionButton icon='refresh' onClick={refreshGopassStores} />
            <RoundActionButton icon='settings' onClick={() => history.replace('/settings')} />
            <RoundActionButton icon='storage' onClick={() => history.replace('/mounts')} />
            <RoundActionButton icon='security' onClick={() => history.replace('/password-health')} />
        </div>
    )
}

export default withRouter(MainNavigationComponent)


================================================
FILE: src/renderer/explorer-app/components/PasswordStrengthInfo.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'

interface PasswordStrengthInfoProps {
    strength: number
    labelContent: any
}

export const PasswordStrengthInfo = ({ strength, labelContent }: PasswordStrengthInfoProps) => (
    <>
        <m.Col s={4}>
            <label className='active'>
                {labelContent}: {strength + ' %'}
            </label>
            <m.ProgressBar progress={strength} />
        </m.Col>
    </>
)


================================================
FILE: src/renderer/explorer-app/pages/AddMountPage.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'
import { RouteComponentProps, withRouter } from 'react-router'
import Gopass, { Mount } from '../../secrets/Gopass'
import { RoundActionButton } from '../../components/RoundActionButton'
import { useSecretsContext } from '../SecretsProvider'
import { useNotificationContext } from '../../common/notifications/NotificationProvider'

function AddMountPage({ history }: RouteComponentProps) {
    const notificationContext = useNotificationContext()
    const secretsContext = useSecretsContext()
    const [mount, setMount] = React.useState<Mount>({ name: '', path: '' })

    const addMount = async () => {
        if (mount.path && mount.name) {
            try {
                await Gopass.addMount(mount)
                await secretsContext.reloadSecretNames()
                history.replace('/mounts')
            } catch (err) {
                if (typeof err === 'string') {
                    if (err === 'duplicate-name') {
                        notificationContext.show({ status: 'ERROR', message: `A mount named "${mount.name}" does already exist` })
                    }

                    if (err.includes('Doubly mounted path')) {
                        notificationContext.show({
                            status: 'ERROR',
                            message: `The path "${mount.path}" is already in use by another mount`
                        })
                    }
                } else {
                    notificationContext.show({
                        status: 'ERROR',
                        message: `Unexpected error while adding mount: ${JSON.stringify(err)}`
                    })
                }
            }
        }
    }

    return (
        <>
            <div style={{ paddingTop: '0.75rem' }}>
                <RoundActionButton icon='arrow_back' onClick={() => history.replace('/mounts')} />
            </div>

            <h4>New Mount</h4>

            <m.CardPanel>Create a new mount that shall be managed by Gopass as a password store from now on.</m.CardPanel>
            <m.Row>
                <m.Input s={12} value={mount.name} onChange={(_: any, value: string) => setMount({ ...mount, name: value })} label='Name' />
                <m.Input s={12} value={mount.path} onChange={(_: any, value: string) => setMount({ ...mount, path: value })} label='Directory path' />

                <m.Col s={12}>
                    <m.Button disabled={!mount.name || !mount.path} onClick={addMount} waves='light'>
                        Save
                    </m.Button>
                </m.Col>
            </m.Row>
        </>
    )
}

export default withRouter(AddMountPage)


================================================
FILE: src/renderer/explorer-app/pages/AddSecretPage.css
================================================
.secret-value-textarea {
    height: 64px;
    font-size: 16px;
    resize: vertical;
    border: 1px solid #9e9e9e;
    margin-top: 5px;
}

.secret-value-field-row {
    margin: 0 0.75rem;
    width: calc(100% - 1.5rem);
}

.secret-value-label {
    fontSize: 0.8rem;
    color: #9e9e9e;
}


================================================
FILE: src/renderer/explorer-app/pages/AddSecretPage.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'
import { RouteComponentProps, withRouter } from 'react-router'
import Gopass from '../../secrets/Gopass'
import { passwordSecretRegex } from '../../secrets/deriveIconFromSecretName'
import { PasswordStrengthInfo } from '../components/PasswordStrengthInfo'
import { PasswordRater } from '../password-health/PasswordRater'

import './AddSecretPage.css'
import { Settings } from '../../common/Settings'
import { useSecretsContext } from '../SecretsProvider'

interface AddSecretPageState {
    name?: string
    value?: string
}

class AddSecretPage extends React.Component<RouteComponentProps, AddSecretPageState> {
    constructor(props: any) {
        super(props)
        this.state = {
            name: undefined,
            value: this.generateRandomValue(Settings.getUserSettings().secretValueLength)
        }
    }

    public render() {
        const { name, value } = this.state
        const nameIndicatesPassword = name ? passwordSecretRegex.test(name) : false
        const entity = nameIndicatesPassword ? 'Password' : 'Secret'
        const nameLabel = `Secret name (${nameIndicatesPassword ? 'detected password' : 'e.g. store/my/new/secret/name'})`
        const valueLabel = `${entity} value`
        const shuffleButtonLabel = `Shuffle ${nameIndicatesPassword ? 'password' : 'value'}`
        const currentPasswordValueRating = PasswordRater.ratePassword(value || '')

        return (
            <>
                <h4>New {entity}</h4>

                <m.CardPanel>
                    Adds new secrets to your Gopass stores. After clicking the Add-button, your new secret will be pushed to remote directly.
                </m.CardPanel>

                <m.Row>
                    <m.Input s={12} value={name} onChange={this.changeName} label={nameLabel} />
                    <div className='secret-value-field-row'>
                        <label className='secret-value-label'>{valueLabel}</label>
                        <textarea className='secret-value-textarea' placeholder={valueLabel} value={value} onChange={this.changeValue} />
                    </div>
                    <PasswordStrengthInfo strength={currentPasswordValueRating.health} labelContent={`${entity} value strength`} />
                    <m.Col s={12}>
                        <m.Button style={{ marginRight: '10px' }} onClick={this.shuffleRandomValue} waves='light'>
                            {shuffleButtonLabel}
                        </m.Button>
                        <m.Button disabled={!name || !value} onClick={this.addSecret} waves='light'>
                            Save
                        </m.Button>
                    </m.Col>
                </m.Row>
            </>
        )
    }

    private changeName = (_: any, name: string) => this.setState({ name })
    private changeValue = (event: any) => this.setState({ value: event.target.value })

    private generateRandomValue = (length: number) => {
        const chars = 'abcdefghijklmnopqrstuvwxyz!@#$%^&*()-+<>ABCDEFGHIJKLMNOP1234567890'
        let randomPassword = ''

        for (let x = 0; x < length; x++) {
            const randomIndex = Math.floor(Math.random() * chars.length)
            randomPassword += chars.charAt(randomIndex)
        }

        return randomPassword
    }

    private shuffleRandomValue = () => this.setState({ value: this.generateRandomValue(Settings.getUserSettings().secretValueLength) })

    private addSecret = async () => {
        const { name, value } = this.state

        if (name && value) {
            const { history } = this.props

            try {
                await Gopass.addSecret(name, value)
                await useSecretsContext().reloadSecretNames()
                history.replace(`/secret/${btoa(name)}?added`)
            } catch (e) {
                console.info('Error during adding a secret', e)
            }
        }
    }
}

export default withRouter(AddSecretPage)


================================================
FILE: src/renderer/explorer-app/pages/HomePage.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'

import { EnvironmentTest } from '../components/EnvironmentTest'
import { LatestVersionInfo } from '../components/LastVersionInfo'
import { ExternalLink } from '../../components/ExternalLink'
import { Settings } from '../../common/Settings'

const OptionalSetupInstructions = () => {
    const { environmentTestSuccessful } = Settings.getSystemSettings()

    return environmentTestSuccessful ? null : (
        <>
            <h4>Environment Test</h4>
            <m.CardPanel>
                <EnvironmentTest />
            </m.CardPanel>
        </>
    )
}

const HomePage = () => {
    const { searchShortcut } = Settings.getUserSettings()

    return (
        <>
            <h3>Welcome to Gopass UI</h3>
            <OptionalSetupInstructions />

            <m.CardPanel>
                Choose a secret from the navigation or use the actions at the top. <LatestVersionInfo />
            </m.CardPanel>

            <h4 className='m-top'>Global search window</h4>
            <m.CardPanel>
                The configured shortcut for the global search window (quick secret clipboard-copying) is:
                <pre>{searchShortcut.replace(/\+/g, ' + ')}</pre>
            </m.CardPanel>

            <h4 className='m-top'>Issues</h4>
            <m.CardPanel>
                Please report any issues and problems to us on <ExternalLink url='https://github.com/codecentric/gopass-ui/issues'>Github</ExternalLink>.<br />
                We are very keen about your feedback and appreciate any help.
            </m.CardPanel>
        </>
    )
}

export default HomePage


================================================
FILE: src/renderer/explorer-app/pages/MountsPage.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'
import { RouteComponentProps, withRouter } from 'react-router'

import Gopass, { Mount } from '../../secrets/Gopass'
import PaginatedTable from '../../components/PaginatedTable'
import { LoadingScreen } from '../../components/loading-screen/LoadingScreen'
import { RoundActionButton } from '../../components/RoundActionButton'
import { useSecretsContext } from '../SecretsProvider'

const MountsPage = ({ history }: RouteComponentProps) => {
    const secretsContext = useSecretsContext()
    const [mounts, setMounts] = React.useState<Mount[] | undefined>(undefined)
    const [loading, setLoading] = React.useState(true)

    const loadMounts = async () => {
        setLoading(true)
        const allMounts = await Gopass.getAllMounts()
        setMounts(allMounts)
        setLoading(false)
    }
    const deleteMount = async (name: string) => {
        await Gopass.deleteMount(name)
        await secretsContext.reloadSecretNames()
        await loadMounts()
    }

    React.useEffect(() => {
        loadMounts()
    }, [])

    return (
        <>
            <div style={{ paddingTop: '0.75rem' }}>
                <RoundActionButton icon='arrow_back' onClick={() => history.replace('/')} />
                <RoundActionButton icon='add' onClick={() => history.replace('/add-mount')} />
            </div>

            <h4>Mounts</h4>
            {loading && <LoadingScreen />}
            {!loading && mounts && mounts.length === 0 && <m.CardPanel>No mounts available.</m.CardPanel>}
            {!loading && mounts && mounts.length > 0 && <MountsTable entries={mounts} deleteRow={deleteMount} />}
        </>
    )
}

const MountsTable = ({ entries, deleteRow }: { entries: Mount[]; deleteRow: (name: string) => Promise<void> }) => {
    const rows = entries.map(entry => ({
        ...entry,
        id: entry.name,
        actions: (
            <>
                <a className='btn-flat' onClick={() => deleteRow(entry.name)}>
                    <m.Icon>delete_forever</m.Icon>
                </a>
            </>
        )
    }))
    const columns = [
        { fieldName: 'name', label: 'Name' },
        { fieldName: 'path', label: 'Directory path' },
        { fieldName: 'actions', label: 'Actions' }
    ]

    return <PaginatedTable columns={columns} rows={rows} />
}

export default withRouter(MountsPage)


================================================
FILE: src/renderer/explorer-app/pages/PasswordHealthPage.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'
import { withRouter, RouteComponentProps } from 'react-router'
import AsyncPasswordHealthCollector, { PasswordHealthCollectionStatus, PasswordSecretHealth } from '../../secrets/AsyncPasswordHealthCollector'
import PaginatedTable from '../../components/PaginatedTable'
import { LoadingScreen } from '../../components/loading-screen/LoadingScreen'
import { PasswordHealthIndicator } from '../password-health/PasswordHealthIndicator'
import { PasswordHealthSummary, PasswordRater } from '../password-health/PasswordRater'

interface PasswordHealthPageState {
    collector: AsyncPasswordHealthCollector
    statusChecker?: number
    status?: PasswordHealthCollectionStatus
}

class PasswordHealthPage extends React.Component<RouteComponentProps, PasswordHealthPageState> {
    constructor(props: any) {
        super(props)
        this.state = {
            collector: new AsyncPasswordHealthCollector()
        }
    }

    public async componentDidMount() {
        const { collector } = this.state
        collector.start()

        const statusChecker = window.setInterval(async () => {
            const status = this.state.collector.getCurrentStatus()
            this.setState({ status })
            if (!status.inProgress) {
                await this.stopStatusChecker()
            }
        }, 100)
        this.setState({ statusChecker })
    }

    public async componentWillUnmount() {
        await this.stopStatusChecker()
    }

    public render() {
        const { status } = this.state

        return (
            <>
                <h4>Password Health</h4>
                {status ? this.renderStatus(status) : <LoadingScreen />}
            </>
        )
    }

    private async stopStatusChecker() {
        const { collector, statusChecker } = this.state
        await collector.stopAndReset()

        if (statusChecker) {
            clearInterval(statusChecker)
        }
    }

    // tslint:disable-next-line
    private renderStatus(status: PasswordHealthCollectionStatus) {
        if (!status.inProgress && status.passwordsCollected === 0) {
            return (
                <p>
                    It seems you don't have passwords in your stores yet. A secret is considered a password if it contains words such as: password, pw, pass,
                    secret, key or similar.
                </p>
            )
        }

        if (status.inProgress && status.passwordsCollected > 0) {
            const progressPercentage = Math.round((status.passwordsCollected / status.totalPasswords) * 100)

            return (
                <>
                    <p>Your passwords are currently being collected and analysed, please wait until ready... {progressPercentage}%</p>
                    <div style={{ width: '60%', minWidth: '200px', marginTop: '30px' }}>
                        <m.ProgressBar progress={progressPercentage} />
                    </div>
                </>
            )
        }

        if (!status.inProgress && status.passwordsCollected > 0 && status.passwordsCollected === status.totalPasswords && !status.error) {
            const overallPasswordHealth = PasswordRater.buildOverallPasswordHealthSummary(status.ratedPasswords)
            const improvablePasswords = overallPasswordHealth.ratedPasswordSecrets.filter(rated => rated.health && rated.health < 100)

            return (
                <>
                    {this.renderOverallPasswordHealth(overallPasswordHealth, improvablePasswords.length)}
                    {this.renderImprovementPotential(improvablePasswords)}
                </>
            )
        }

        if (!status.inProgress && status.error) {
            return <p>Something went wrong here: {status.error.message}</p>
        }
    }

    private renderOverallPasswordHealth(overallPasswordHealth: PasswordHealthSummary, improvablePasswordsAmount: number) {
        return (
            <div className='row'>
                <div className='col s12'>
                    <div className='card-panel z-depth-1'>
                        <div className='row valign-wrapper'>
                            <div className='col s2'>
                                <PasswordHealthIndicator health={overallPasswordHealth.health} />
                            </div>
                            <div className='col s10'>
                                This is the average health for your passwords.
                                {improvablePasswordsAmount > 0 ? ` There are ${improvablePasswordsAmount} suggestions available.` : ''}
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        )
    }

    private renderImprovementPotential(improvablePasswords: PasswordSecretHealth[]) {
        return (
            improvablePasswords.length > 0 && (
                <>
                    <h4 className='m-top'>Improvement Potential</h4>
                    <PaginatedTable
                        columns={[
                            { fieldName: 'name', label: 'Name' },
                            { fieldName: 'health', label: 'Health' },
                            { fieldName: 'rulesToImprove', label: 'Rules to improve' }
                        ]}
                        rows={improvablePasswords.map(rated => ({
                            id: rated.name,
                            name: <a onClick={this.onSecretClick(rated.name)}>{rated.name}</a>,
                            health: `${rated.health}`,
                            rulesToImprove: `${rated.failedRulesCount}`
                        }))}
                    />
                </>
            )
        )
    }

    private onSecretClick = (secretName: string) => () => this.props.history.replace(`/secret/${btoa(secretName)}`)
}

export default withRouter(PasswordHealthPage)


================================================
FILE: src/renderer/explorer-app/pages/SettingsPage.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'
import * as isValidElectronShortcut from 'electron-is-accelerator'

import { EnvironmentTest } from '../components/EnvironmentTest'
import { Settings } from '../../common/Settings'
import { ExternalLink } from '../../components/ExternalLink'

interface SettingsPageState {
    environmentTestSuccessful: boolean
    secretValueLength: number
    searchShortcut: string
}

export default function SettingsPage() {
    const [state, setState] = React.useState<SettingsPageState | undefined>(undefined)
    const [isShortcutValidationError, setShortcutValidationError] = React.useState<boolean>(false)

    React.useEffect(() => {
        const { environmentTestSuccessful } = Settings.getSystemSettings()
        const { secretValueLength, searchShortcut } = Settings.getUserSettings()
        setState({ environmentTestSuccessful, secretValueLength, searchShortcut })
    }, [])

    if (!state) {
        return null
    }

    return (
        <>
            <h4>Settings</h4>
            <m.CardPanel>
                <m.Row>
                    <m.Col s={12}>
                        <label className='active'>Characters for generated secrets: {state.secretValueLength}</label>
                        <p style={{ width: '33%' }} className='range-field'>
                            <input
                                type='range'
                                defaultValue={`${state.secretValueLength}`}
                                min='6'
                                onChange={event => {
                                    const value = parseInt(event.target.value, 10)
                                    Settings.updateUserSettings({ secretValueLength: value })
                                }}
                                max='200'
                            />
                        </p>
                    </m.Col>
                    <m.Input
                        s={4}
                        error={isShortcutValidationError ? 'error' : undefined}
                        defaultValue={state.searchShortcut}
                        onChange={(_: any, value: string) => {
                            const isValid = isValidElectronShortcut(value)
                            if (isValid) {
                                Settings.updateUserSettings({ searchShortcut: value })
                            }

                            setShortcutValidationError(!isValid)
                        }}
                        label={
                            <>
                                <span>Global search window shortcut</span>{' '}
                                <ExternalLink url='https://www.electronjs.org/docs/api/accelerator#available-modifiers'>(see all options)</ExternalLink>
                            </>
                        }
                    />
                </m.Row>
            </m.CardPanel>

            <h4>Environment Test</h4>
            {state.environmentTestSuccessful && <strong>🙌 The last test was successful 🙌</strong>}
            <m.CardPanel>
                <EnvironmentTest />
            </m.CardPanel>
        </>
    )
}


================================================
FILE: src/renderer/explorer-app/pages/details/HistoryTable.tsx
================================================
import * as React from 'react'
import * as dateformat from 'dateformat'

import PaginatedTable from '../../../components/PaginatedTable'
import { HistoryEntry } from '../../../secrets/Gopass'

export interface HistoryTableProps {
    entries: HistoryEntry[]
}

export function HistoryTable({ entries }: HistoryTableProps) {
    const rows = entries.map(entry => ({
        ...entry,
        id: entry.hash,
        timestamp: dateformat(new Date(entry.timestamp), 'yyyy-mm-dd HH:MM')
    }))
    const columns = [
        { fieldName: 'timestamp', label: 'Time' },
        { fieldName: 'author', label: 'Author' },
        { fieldName: 'message', label: 'Message' }
    ]

    return <PaginatedTable columns={columns} rows={rows} />
}


================================================
FILE: src/renderer/explorer-app/pages/details/SecretDetailsPage.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'
import { RouteComponentProps, withRouter } from 'react-router'

import Gopass, { HistoryEntry } from '../../../secrets/Gopass'
import { passwordSecretRegex } from '../../../secrets/deriveIconFromSecretName'
import { LoadingScreen } from '../../../components/loading-screen/LoadingScreen'
import { useCopySecretToClipboard } from '../../../secrets/useCopySecretToClipboard'
import { HistoryTable } from './HistoryTable'
import PasswordRatingComponent from '../../password-health/PasswordRatingComponent'
import { useSecretsContext } from '../../SecretsProvider'

interface SecretDetailsPageProps extends RouteComponentProps {
    secretName: string
    isAdded?: boolean
}

// todo: make this configurable in the application settings
const DISPLAY_SECRET_VALUE_BY_DEFAULT = false

/* tslint:disable */
function SecretDetailsPage({ secretName, isAdded, history }: SecretDetailsPageProps) {
    const [secretValue, setSecretValue] = React.useState('')
    const [historyEntries, setHistoryEntries] = React.useState<HistoryEntry[]>([])
    const [loading, setLoading] = React.useState(true)
    const [isPassword, setIsPassword] = React.useState(false)
    const [displaySecretValue, setDisplaySecretValue] = React.useState(DISPLAY_SECRET_VALUE_BY_DEFAULT)
    const [editedSecretValue, setEditedSecretValue] = React.useState<string | undefined>(undefined)
    const [queryDeletion, setQueryDeletion] = React.useState(false)

    React.useEffect(() => {
        setLoading(true)

        Promise.all([Gopass.show(secretName), Gopass.history(secretName)]).then(([newSecretValue, newHistoryEntries]) => {
            setSecretValue(newSecretValue)
            setHistoryEntries(newHistoryEntries)
            setLoading(false)
            setQueryDeletion(false)
            setEditedSecretValue(undefined)
            setDisplaySecretValue(DISPLAY_SECRET_VALUE_BY_DEFAULT)
        })
    }, [secretName])

    React.useEffect(() => {
        setIsPassword(passwordSecretRegex.test(secretName))
    }, [secretValue])

    const querySecretDeletion = () => setQueryDeletion(true)
    const denySecretDeletion = () => setQueryDeletion(false)
    const confirmSecretDeletion = () => Gopass.deleteSecret(secretName).then(() => history.replace('/'))
    const deletionModeButtons = queryDeletion ? (
        <>
            <a className='link' onClick={denySecretDeletion}>
                NO, keep it!
            </a>
            <a
                className='link'
                onClick={async () => {
                    await confirmSecretDeletion()
                    await useSecretsContext().reloadSecretNames()
                }}
            >
                Sure!
            </a>
        </>
    ) : (
        <a className='link' onClick={querySecretDeletion}>
            Delete
        </a>
    )

    const querySecretEditing = () => setEditedSecretValue(secretValue)
    const discardSecretEditing = () => setEditedSecretValue(undefined)
    const onNewSecretValueChange = (event: any) => setEditedSecretValue(event.target.value)
    const saveEditedSecretValue = async () => {
        if (editedSecretValue && editedSecretValue !== secretValue) {
            await Gopass.editSecret(secretName, editedSecretValue)
            setSecretValue(editedSecretValue)
        }

        setEditedSecretValue(undefined)
    }
    const editModeButtons = (
        <>
            <a className='link' onClick={discardSecretEditing}>
                Discard
            </a>
            <a className='link' onClick={saveEditedSecretValue}>
                Save changes
            </a>
        </>
    )

    const inEditMode = editedSecretValue !== undefined
    const copySecretToClipboard = useCopySecretToClipboard()
    const cardActions = [
        <a key='toggle-display' className='link' onClick={() => setDisplaySecretValue(!displaySecretValue)}>
            {displaySecretValue ? 'Hide' : 'Show'}
        </a>,
        <a key='copy-clipboard' className='link' onClick={() => copySecretToClipboard(secretName)}>
            Copy to clipboard
        </a>,
        inEditMode ? (
            <span key='edit-secret-mode-actions'>{editModeButtons}</span>
        ) : (
            <span key='view-secret-mode-actions'>
                <a className='link' onClick={querySecretEditing}>
                    Edit
                </a>
                {deletionModeButtons}
            </span>
        )
    ]
    const secretValueToDisplay = displaySecretValue ? secretValue : '*******************'
    const valueToDisplay = inEditMode ? editedSecretValue : secretValueToDisplay
    const linesRequired = (valueToDisplay || '').split('\n').length

    return loading ? (
        <>
            <h4>Secret {isAdded && <m.Icon small>fiber_new</m.Icon>}</h4>
            <LoadingScreen />
        </>
    ) : (
        <>
            <h4>Secret {isAdded && <m.Icon small>fiber_new</m.Icon>}</h4>
            <m.Card title={secretName} actions={cardActions}>
                <textarea
                    style={{
                        color: '#212121',
                        fontSize: 16,
                        borderBottom: '1px dotted rgba(0, 0, 0, 0.42)',
                        height: 21 * linesRequired,
                        borderTop: 'none',
                        borderRight: 'none',
                        borderLeft: 'none'
                    }}
                    value={valueToDisplay}
                    disabled={!inEditMode}
                    onChange={onNewSecretValueChange}
                    ref={input => input && input.focus()}
                />
            </m.Card>

            {isPassword && (
                <>
                    <h4 className='m-top'>Password Strength</h4>
                    <PasswordRatingComponent secretValue={secretValue} />
                </>
            )}

            <h4 className='m-top'>History</h4>
            <HistoryTable entries={historyEntries} />
        </>
    )
}

export default withRouter(SecretDetailsPage)


================================================
FILE: src/renderer/explorer-app/password-health/PasswordHealthIndicator.tsx
================================================
import * as React from 'react'

export const passwordStrengthColorExtractor = (health: number): string => {
    if (health >= 70) {
        return 'green'
    }
    if (health >= 50) {
        return 'yellow'
    }

    return 'red'
}

export const PasswordHealthIndicator = ({ health }: { health: number }) => (
    <div className={`password-strength-sum ${passwordStrengthColorExtractor(health)}`}>
        <span>{health}</span>
    </div>
)


================================================
FILE: src/renderer/explorer-app/password-health/PasswordHealthRules.ts
================================================
import { PasswordHealthRule } from './PasswordRule'

const minimumLengthRule: PasswordHealthRule = {
    matcher: (password: string) => password.length >= 10,
    name: 'Minimal length of 10 characters',
    description: 'Good passwords should have a minimum of 10 characters'
}

const lowerCaseRule: PasswordHealthRule = {
    matcher: (password: string) => /(?=.*[a-z])/.test(password),
    name: 'Lowercase letters',
    description: 'Make sure to have a least one lowercase letter inside'
}

const upperCaseRule: PasswordHealthRule = {
    matcher: (password: string) => /(?=.*[A-Z])/.test(password),
    name: 'Uppercase letters',
    description: 'Make sure it contains at least one uppercase letter'
}

const specialCharRule: PasswordHealthRule = {
    matcher: (password: string) => /(?=.*\W)/.test(password),
    name: 'Special characters',
    description: 'Use special characters to make your password stronger'
}

const numbersRule: PasswordHealthRule = {
    matcher: (password: string) => /(?=.*\d)/.test(password),
    name: 'Numbers',
    description: 'Make sure to use at least one number to improve your password'
}

const noRepetitiveCharactersRule: PasswordHealthRule = {
    matcher: (password: string) => !!password && !/(.)\1{2,}/.test(password),
    name: 'No repetitive characters',
    description: 'Passwords are better if characters are not repeated often (sequence of three or more).'
}

export const allPasswordHealthRules: PasswordHealthRule[] = [
    minimumLengthRule,
    lowerCaseRule,
    upperCaseRule,
    specialCharRule,
    numbersRule,
    noRepetitiveCharactersRule
]


================================================
FILE: src/renderer/explorer-app/password-health/PasswordRater.ts
================================================
import { PasswordHealthRule, PasswordHealthRuleInfo } from './PasswordRule'
import { allPasswordHealthRules } from './PasswordHealthRules'
import { PasswordSecretHealth } from '../../secrets/AsyncPasswordHealthCollector'

export interface PasswordRatingResult {
    totalRulesCount: number
    failedRules: PasswordHealthRuleInfo[]
    health: number
}

const ruleToMeta = (rule: PasswordHealthRule): PasswordHealthRuleInfo => ({ name: rule.name, description: rule.description })
const calculateHealth = (failed: number, total: number) => Math.round(100 - (failed / total) * 100)

export interface PasswordHealthSummary {
    health: number
    ratedPasswordSecrets: PasswordSecretHealth[]
}

export class PasswordRater {
    public static ratePassword(password: string): PasswordRatingResult {
        const totalRulesCount = allPasswordHealthRules.length
        const failedRules = allPasswordHealthRules.filter(rule => !rule.matcher(password)).map(ruleToMeta)

        return {
            totalRulesCount,
            failedRules,
            health: calculateHealth(failedRules.length, totalRulesCount)
        }
    }

    public static buildOverallPasswordHealthSummary(passwordHealths: PasswordSecretHealth[]): PasswordHealthSummary {
        const pwHealthSum = passwordHealths.map(h => h.health).reduce((a: number, b: number) => a + b, 0)

        return {
            health: Math.round(pwHealthSum / passwordHealths.length),
            ratedPasswordSecrets: passwordHealths
        }
    }
}


================================================
FILE: src/renderer/explorer-app/password-health/PasswordRatingComponent.css
================================================
ol.failed-rules-list {
    padding-left: 17px;
}

ol.failed-rules-list > li > strong {
    font-weight: 500;
}


================================================
FILE: src/renderer/explorer-app/password-health/PasswordRatingComponent.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'

import { PasswordRater } from './PasswordRater'
import { PasswordHealthRuleInfo } from './PasswordRule'
import { PasswordHealthIndicator } from './PasswordHealthIndicator'

import './PasswordRatingComponent.css'

export interface PasswordRatingComponentProps {
    secretValue: string
}

export const FailedRulesList = ({ failedRules }: { failedRules: PasswordHealthRuleInfo[] }) => (
    <>
        {failedRules.length > 0 ? (
            <>
                <p>
                    You could improve <strong>{failedRules.length}</strong> characteristica of this password.
                </p>
                <ol className='failed-rules-list'>
                    {failedRules.map((failedRule: PasswordHealthRuleInfo, index: number) => (
                        <li key={index}>
                            <strong>{failedRule.name}:</strong> {failedRule.description}
                        </li>
                    ))}
                </ol>
            </>
        ) : (
            <p>Good job! This secret satisfies all basic criteria for a potentially good password.</p>
        )}
    </>
)

const PasswordRatingComponent = ({ secretValue }: PasswordRatingComponentProps) => {
    const { health, failedRules } = PasswordRater.ratePassword(secretValue)

    return (
        <m.Row>
            <m.Col s={12}>
                <div className='card-panel z-depth-1'>
                    <div className='row valign-wrapper'>
                        <div className='col s2'>
                            <PasswordHealthIndicator health={health} />
                        </div>
                        <div className='col s10'>
                            <FailedRulesList failedRules={failedRules} />
                        </div>
                    </div>
                </div>
            </m.Col>
        </m.Row>
    )
}

export default PasswordRatingComponent


================================================
FILE: src/renderer/explorer-app/password-health/PasswordRule.d.ts
================================================
export interface PasswordHealthRuleInfo {
    name: string
    description: string
}

export interface PasswordHealthRule extends PasswordHealthRuleInfo {
    matcher: (password: string) => boolean
}


================================================
FILE: src/renderer/explorer-app/side-navigation/SecretExplorer.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'
import * as KeyboardEventHandler from 'react-keyboard-event-handler'

import { RouteComponentProps, withRouter } from 'react-router'
import SecretTree from './SecretTree'
import { useSecretsContext } from '../SecretsProvider'

const SecretExplorer = ({ history }: RouteComponentProps) => {
    const { tree, applySearchToTree, reloadSecretNames, searchValue } = useSecretsContext()
    const navigateToSecretDetailView = (secretName: string) => history.replace(`/secret/${btoa(secretName)}`)
    const clearSearch = () => applySearchToTree('')

    React.useEffect(() => {
        reloadSecretNames()
    }, [])

    return (
        <div className='secret-explorer'>
            <KeyboardEventHandler handleKeys={['esc']} handleFocusableElements onKeyEvent={clearSearch} />
            <div className='search-bar'>
                <m.Input
                    value={searchValue}
                    placeholder='Search...'
                    onChange={(_: any, updatedSearchValue: string) => {
                        applySearchToTree(updatedSearchValue)
                    }}
                />
            </div>
            <SecretTree tree={tree} onSecretClick={navigateToSecretDetailView} />
        </div>
    )
}

export default withRouter(SecretExplorer)


================================================
FILE: src/renderer/explorer-app/side-navigation/SecretTree.tsx
================================================
import * as React from 'react'
import TreeComponent, { Tree } from '../../components/tree/TreeComponent'

export interface SecretTreeViewerProps {
    onSecretClick: (name: string) => void
    tree: Tree
}

export default class SecretTreeViewer extends React.Component<SecretTreeViewerProps, {}> {
    public render() {
        return <TreeComponent tree={this.props.tree} onLeafClick={this.props.onSecretClick} />
    }
}


================================================
FILE: src/renderer/explorer-app/side-navigation/SecretsDirectoryService.ts
================================================
import { Tree } from '../../components/tree/TreeComponent'

export default class SecretsDirectoryService {
    public static secretPathsToTree(secretPaths: string[], previousTree: Tree, openAllEntries: boolean): Tree {
        const directory = SecretsDirectoryService.secretPathsToDirectory(secretPaths)
        return SecretsDirectoryService.directoryToTree(directory, previousTree, openAllEntries)
    }

    /**
     * Convert
     *  from: "xyz/service/someServiceName/db/password"
     *  to: "{ xyz: { service: { someServiceName: { db: { password: {} } } } }  }"
     */
    private static secretPathsToDirectory(secretPaths: string[]): any {
        const directory: { [key: string]: any } = {}

        for (const secretPath of secretPaths) {
            const segments = secretPath.split('/')

            let tempDir = directory
            segments.forEach((segment: string, index: number) => {
                if (!tempDir[segment]) {
                    tempDir[segment] = index + 1 === segments.length ? secretPath : {}
                }

                tempDir = tempDir[segment]
            })
        }

        return directory
    }

    private static getToggledPathsFromTree(tree: Tree): string[] {
        const paths: string[] = []

        for (const child of tree.children || []) {
            if (child.toggled) {
                paths.push(child.path)
            }
            paths.push(...SecretsDirectoryService.getToggledPathsFromTree(child))
        }

        return paths
    }

    /**
     * Convert
     *  from: "{ xyz: { service: { someServiceName: { db: { password: {} } } } }  }"
     *  to Tree interface
     */
    private static directoryToTree(directory: any, previousTree: Tree, openAllEntries: boolean): Tree {
        const toggledPaths = SecretsDirectoryService.getToggledPathsFromTree(previousTree)
        const children = SecretsDirectoryService.getChildren(directory, toggledPaths, true, openAllEntries)
        const tree: Tree = {
            name: 'Stores',
            toggled: true,
            path: '',
            children
        }

        return tree
    }

    private static getChildren(
        directory: any | string,
        toggledPaths: string[],
        toggled: boolean = false,
        toggleAll: boolean = false,
        parentPath = ''
    ): Tree[] | undefined {
        if (!(directory instanceof Object)) {
            return undefined
        }
        const childDirNames = Object.keys(directory).filter(key => key !== '')

        if (childDirNames.length === 0) {
            return undefined
        }

        return childDirNames.map(name => {
            const path = parentPath.length > 0 ? parentPath + '/' + name : name
            const toggledFromPreviousTree = toggledPaths.includes(path)
            const toggledPathsLeft = toggledFromPreviousTree ? toggledPaths.filter(p => p !== path) : toggledPaths
            const children = SecretsDirectoryService.getChildren(directory[name], toggledPathsLeft, false, toggleAll, path)

            return {
                name,
                path,
                children,
                toggled: toggleAll || toggled || toggledFromPreviousTree
            }
        })
    }
}


================================================
FILE: src/renderer/explorer-app/side-navigation/SecretsFilterService.ts
================================================
export default class SecretsFilterService {
    public static filterBySearch(secretNames: string[], searchValue: string): string[] {
        const searchValues = searchValue
            .split(' ')
            .map(value => value.trim())
            .filter(value => value !== '')

        return secretNames.filter(SecretsFilterService.filterMatchingSecrets(searchValues))
    }

    private static filterMatchingSecrets = (searchValues: string[]) => (secretName: string) => {
        if (searchValues.length > 0) {
            return searchValues.every(value => secretName.includes(value))
        }

        return true
    }
}


================================================
FILE: src/renderer/explorer-app.tsx
================================================
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { HashRouter as Router } from 'react-router-dom'
import { AppContainer } from 'react-hot-loader'

import ExplorerApplication from './explorer-app/ExplorerApplication'

import 'materialize-css/dist/css/materialize.css'
import 'material-design-icons/iconfont/material-icons.css'
import 'animate.css/animate.css'
import { SecretsProvider } from './explorer-app/SecretsProvider'

const mainElement = document.createElement('div')
document.body.appendChild(mainElement)

ReactDOM.render(
    <AppContainer>
        <Router>
            <SecretsProvider>
                <ExplorerApplication />
            </SecretsProvider>
        </Router>
    </AppContainer>,
    mainElement
)


================================================
FILE: src/renderer/search-app/CollectionItems.tsx
================================================
import * as m from 'react-materialize'
import * as React from 'react'

import { SecretText } from './SecretText'

export interface CollectionItemProps {
    filteredSecretNames: string[]
    selectedItemIndex: number
    highlightRegExp?: RegExp
    onItemClick: (secretKey: string) => void
}

export function CollectionItems({ filteredSecretNames, selectedItemIndex, onItemClick, highlightRegExp }: CollectionItemProps) {
    return (
        <>
            {filteredSecretNames.map((secretKey, i) => {
                const secretPath = secretKey.split('/')
                const isSelected = i === selectedItemIndex ? 'selected' : undefined

                return (
                    <m.CollectionItem key={`entry-${i}`} className={isSelected} onClick={() => onItemClick(secretKey)}>
                        <SecretText secretPath={secretPath} highlightRegExp={highlightRegExp} />
                    </m.CollectionItem>
                )
            })}
        </>
    )
}


================================================
FILE: src/renderer/search-app/SearchApplication.css
================================================
strong {
    font-weight: bolder;
}

.collection .collection-item:hover {
    background-color: #EBEBEB;
}

.collection .collection-item.selected {
    background-color: #DCDCDC;
}

.collection .collection-item {
    cursor: pointer;
}

.link {
    cursor: pointer
}


================================================
FILE: src/renderer/search-app/SearchApplication.tsx
================================================
import * as React from 'react'
import * as m from 'react-materialize'

import SearchResults from './SearchResults'
import Notification from '../common/notifications/Notification'
import NotificationProvider from '../common/notifications/NotificationProvider'

import './SearchApplication.css'

const NAVIGATION_KEYS = ['ArrowUp', 'ArrowDown', 'Enter', 'Tab']

const preventNavigationKeys = (event: any) => {
    if (NAVIGATION_KEYS.includes(event.key)) {
        event.preventDefault()
    }
}

export function SearchApplication() {
    const [searchValue, setSearchValue] = React.useState('')

    React.useEffect(() => {
        const element = document.getElementById('search')

        if (element) {
            element.focus()
            element.click()
        }
    }, [])

    const onChange = (_: any, newValue: string) => {
        setSearchValue(newValue)
    }

    return (
        <NotificationProvider>
            <Notification dismissTimeout={3000} />
            <m.Row>
                <m.Col s={12}>
                    <m.Input id='search' placeholder='Search...' onChange={onChange} onKeyDown={preventNavigationKeys} s={12} />
                </m.Col>
            </m.Row>
            <m.Row>
                <m.Col s={12}>
                    <SearchResults search={searchValue} />
                </m.Col>
            </m.Row>
        </NotificationProvider>
    )
}


================================================
FILE: src/renderer/search-app/SearchResults.tsx
================================================
import * as React from 'react'
import { ipcRenderer } from 'electron'
import * as m from 'react-materialize'
import * as KeyboardEventHandler from 'react-keyboard-event-handler'

import Gopass from '../secrets/Gopass'
import { useCopySecretToClipboard } from '../secrets/useCopySecretToClipboard'
import { CollectionItems } from './CollectionItems'

const NUMBER_OF_SEARCH_RESULTS = 15

export interface SearchResultsProps {
    search: string
}

export default function SearchResults(props: SearchResultsProps) {
    const [allSecretNames, setAllSecretNames] = React.useState<string[]>([])
    const [filteredSecretNames, setFilteredSecretNames] = React.useState<string[]>([])
    const [selectedItemIndex, setSelectedItemIndex] = React.useState(0)
    const [highlightRegExp, setHighlightRegExp] = React.useState<RegExp | undefined>()

    const copySecretToClipboard = useCopySecretToClipboard()

    const updateFilteredSecrets = (search?: string) => {
        if (search) {
            const searchValues = search.split(' ').map(searchValue => searchValue.trim())

            setFilteredSecretNames(allSecretNames.filter(filterMatchingSecrets(searchValues)).slice(0, NUMBER_OF_SEARCH_RESULTS))
            setHighlightRegExp(new RegExp(`(${searchValues.join('|')})`, 'g'))
        } else {
            setFilteredSecretNames(allSecretNames.slice(0, NUMBER_OF_SEARCH_RESULTS))
            setHighlightRegExp(undefined)
        }
        setSelectedItemIndex(0)
    }

    React.useEffect(() => {
        Gopass.getAllSecretNames().then(newSecretNames => {
            setAllSecretNames(newSecretNames)
        })
    }, [])

    React.useEffect(() => {
        updateFilteredSecrets()
    }, [allSecretNames])

    React.useEffect(() => {
        updateFilteredSecrets(props.search)
    }, [props.search])

    const onKeyEvent = (key: string, event: any) => {
        switch (key) {
            case 'shift+tab':
            case 'up':
                if (selectedItemIndex > 0) {
                    setSelectedItemIndex(selectedItemIndex - 1)
                    event.preventDefault()
                }
                break

            case 'down':
            case 'tab':
                if (selectedItemIndex < filteredSecretNames.length - 1) {
                    setSelectedItemIndex(selectedItemIndex + 1)
                    event.preventDefault()
                }
                break

            case 'enter':
                const secretKey = filteredSecretNames[selectedItemIndex]
                if (secretKey) {
                    copySecretToClipboard(secretKey)
                }

                event.preventDefault()
                break

            case 'esc':
                ipcRenderer.send('hideSearchWindow')
                break
            default:
                console.error('This should not happen ;-) Please verify the "handleKeys" prop from the "KeyboardEventHandler"')
                break
        }
    }

    const onSelectCollectionItem = (secretKey: string) => () => {
        copySecretToClipboard(secretKey)
    }

    return (
        <>
            <KeyboardEventHandler handleKeys={['up', 'shift+tab', 'down', 'tab', 'enter', 'esc']} handleFocusableElements onKeyEvent={onKeyEvent} />
            <m.Collection>
                <CollectionItems
                    filteredSecretNames={filteredSecretNames}
                    selectedItemIndex={selectedItemIndex}
                    highlightRegExp={highlightRegExp}
                    onItemClick={onSelectCollectionItem}
                />
            </m.Collection>
        </>
    )
}

function filterMatchingSecrets(searchValues: string[]) {
    return (secretName: string) => searchValues.every(searchValue => secretName.toLowerCase().includes(searchValue.toLowerCase()))
}


================================================
FILE: src/renderer/search-app/SearchResultsView.tsx
================================================


================================================
FILE: src/renderer/search-app/SecretText.tsx
================================================
import * as React from 'react'
import * as replace from 'string-replace-to-array'

export interface SecretTextProps {
    secretPath: string[]
    highlightRegExp?: RegExp
}

const getHighlightedSegment = (segment: string, highlightRegExp?: RegExp) => {
    if (!highlightRegExp) {
        return segment
    }

    return replace(segment, highlightRegExp, (_: any, match: string, offset: number) => {
        return <mark key={`highlight-${segment}-${offset}`}>{match}</mark>
    })
}

export function SecretText({ secretPath, highlightRegExp }: SecretTextProps) {
    return (
        <>
            {secretPath.reduce((result: string[], segment, currentIndex) => {
                const extendedResult = result.concat(getHighlightedSegment(segment, highlightRegExp))

                if (currentIndex < secretPath.length - 1) {
                    extendedResult.push(' > ')
                }

                return extendedResult
            }, [])}
        </>
    )
}


================================================
FILE: src/renderer/search-app.tsx
================================================
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { AppContainer } from 'react-hot-loader'

import { SearchApplication } from './search-app/SearchApplication'

import 'materialize-css/dist/css/materialize.css'
import 'material-design-icons/iconfont/material-icons.css'

const mainElement = document.createElement('div')
document.body.appendChild(mainElement)

ReactDOM.render(
    <AppContainer>
        <SearchApplication />
    </AppContainer>,
    mainElement
)


================================================
FILE: src/renderer/secrets/AsyncPasswordHealthCollector.ts
================================================
import Gopass from './Gopass'
import { sortBy } from 'lodash'
import { passwordSecretRegex } from './deriveIconFromSecretName'
import { PasswordRater } from '../explorer-app/password-health/PasswordRater'

export interface PasswordSecretHealth {
    name: string
    health: number
    failedRulesCount: number
}

export interface PasswordHealthCollectionStatus {
    totalPasswords: number
    passwordsCollected: number
    inProgress: boolean
    ratedPasswords: PasswordSecretHealth[]
    error: Error | undefined
}

export default class AsyncPasswordHealthCollector {
    private status: PasswordHealthCollectionStatus = {
        totalPasswords: 0,
        passwordsCollected: 0,
        inProgress: false,
        ratedPasswords: [],
        error: undefined
    }

    public async start() {
        const passwordSecretNames = await this.initializePasswordSecretNames(true)

        try {
            await this.collectPasswordHealthOneAfterAnother(passwordSecretNames)
            this.status.inProgress = false
        } catch (e) {
            this.status.error = e
            this.status.inProgress = false
        }
    }

    public async stopAndReset() {
        await this.initializePasswordSecretNames(false)
    }

    public getCurrentStatus(): PasswordHealthCollectionStatus {
        this.status.ratedPasswords = sortBy(this.status.ratedPasswords, (rp: PasswordSecretHealth) => rp.health)
        return this.status
    }

    private async initializePasswordSecretNames(inProgress: boolean): Promise<string[]> {
        this.status.inProgress = inProgress
        const passwordSecretNames = await this.getAllSecretsContainingPasswords()
        this.status.ratedPasswords = []
        this.status.error = undefined
        this.status.totalPasswords = passwordSecretNames.length

        return passwordSecretNames
    }

    private async getAllSecretsContainingPasswords(): Promise<string[]> {
        const allSecretNames = await Gopass.getAllSecretNames()
        return allSecretNames.filter(secretName => passwordSecretRegex.test(secretName))
    }

    private collectPasswordHealthOneAfterAnother = async (passwordSecretNames: string[]) => {
        this.status.ratedPasswords = []

        // use for-loop to do only one Gopass lookup at a time
        for (const name of passwordSecretNames) {
            if (this.status.inProgress) {
                const value = await Gopass.show(name)
                const { health, failedRules } = PasswordRater.ratePassword(value)

                this.status.ratedPasswords.push({ name, health, failedRulesCount: failedRules.length })
                this.status.passwordsCollected++
            } else {
                return
            }
        }
    }
}


================================================
FILE: src/renderer/secrets/Gopass.ts
================================================
import { ipcRenderer } from 'electron'

export interface HistoryEntry {
    hash: string
    author: string
    timestamp: string
    message: string
}

export interface Mount {
    name: string
    path: string
}

const lineSplitRegex = /\r?\n/
const isDefined = (value: string) => !!value
const escapeShellValue = (value: string) => value.replace(/(["'$`\\])/g, '\\$1')

export default class Gopass {
    public static executionId = 1

    public static async copy(key: string): Promise<string> {
        return Gopass.execute(`show -c "${escapeShellValue(key)}"`)
    }

    public static async show(key: string): Promise<string> {
        return Gopass.execute(`show -f "${escapeShellValue(key)}"`)
    }

    public static async history(key: string): Promise<HistoryEntry[]> {
        try {
            return (await Gopass.execute(`history "${escapeShellValue(key)}"`))
                .split(lineSplitRegex)
                .filter(isDefined)
                .map(historyLine => {
                    const lineSplit = historyLine.split(' - ')

                    return {
                        hash: lineSplit[0],
                        author: lineSplit[1],
                        timestamp: lineSplit[2],
                        message: lineSplit[3]
                    }
                })
        } catch {
            return []
        }
    }

    public static async getMyRecipientId(): Promise<string | undefined> {
        const recipientIdLine = (await Gopass.execute('recipients')).split(lineSplitRegex).find(line => line.includes('└──'))
        if (recipientIdLine) {
            const lineSplit = recipientIdLine.split(' ')
            return lineSplit[4].replace('0x', '')
        }
    }

    public static async getAllMounts(): Promise<Mount[]> {
        try {
            return (await Gopass.execute('mounts'))
                .split(lineSplitRegex)
                .filter(line => line.includes('└──') || line.includes('├──'))
                .map(mountLine => {
                    const lineSplit = mountLine.split(' ')

                    return {
                        name: lineSplit[1],
                        path: lineSplit[2].replace(/[{()}]/g, '')
                    }
                })
        } catch {
            return []
        }
    }

    public static async addMount(mount: Mount) {
        const myRecipientId = await Gopass.getMyRecipientId()
        if (myRecipientId) {
            const result = await Gopass.execute('mounts add', [
                `"${escapeShellValue(mount.name)}"`,
                `"${escapeShellValue(mount.path)}"`,
                `-i "${myRecipientId}"`
            ])
            if (result.includes('is already mounted')) {
                // tslint:disable-next-line
                throw 'duplicate-name'
            }
        } else {
            throw new Error('Own GPG recipient ID could not be determined')
        }
    }

    public static async deleteMount(name: string) {
        await Gopass.execute('mounts rm', [`"${escapeShellValue(name)}"`])
    }

    public static async sync(): Promise<void> {
        await Gopass.execute('sync')
    }

    public static async getAllSecretNames(): Promise<string[]> {
        const flatSecrets = await Gopass.execute('list', ['--flat'])

        return flatSecrets.split(lineSplitRegex).filter(isDefined)
    }

    public static async addSecret(name: string, value: string): Promise<void> {
        await Gopass.execute('insert', [`"${escapeShellValue(name.trim())}"`], escapeShellValue(value))
    }

    public static async editSecret(name: string, newValue: string): Promise<void> {
        await Gopass.execute('insert', ['--force', `"${escapeShellValue(name.trim())}"`], escapeShellValue(newValue))
    }

    public static async deleteSecret(name: string): Promise<void> {
        await Gopass.execute('rm', ['--force', `"${escapeShellValue(name)}"`])
    }

    private static execute(command: string, args?: string[], pipeTextInto?: string): Promise<string> {
        // tslint:disable-next-line

        const result = new Promise<string>((resolve, reject) => {
            ipcRenderer.once(`gopass-answer-${Gopass.executionId}`, (_: Event, value: any) => {
                if (value.status === 'ERROR') {
                    reject(value.payload)
                } else {
                    resolve(value.payload)
                }
            })
        })

        ipcRenderer.send('gopass', { executionId: Gopass.executionId, command, args, pipeTextInto })

        Gopass.executionId++

        return result
    }
}


================================================
FILE: src/renderer/secrets/deriveIconFromSecretName.ts
================================================
export interface SecretIconMapping {
    regex: RegExp
    icon: string
}

export const passwordSecretRegex: RegExp = /(password|pw|pass|secret|key$|passphrase|certificate)/

const secretIconMappings: SecretIconMapping[] = [
    {
        regex: passwordSecretRegex,
        icon: 'lock'
    },
    {
        regex: /(user|name|id)/,
        icon: 'person'
    },
    {
        regex: /(note|comment|misc)/,
        icon: 'comment'
    },
    {
        regex: /(uri|url|link|connection)/,
        icon: 'filter_center_focus'
    }
]

export const deriveIconFromSecretName = (secretName: string) => {
    let iconType = 'comment'

    secretIconMappings.forEach((mapping: SecretIconMapping) => {
        const hasIconMapping = mapping.regex.test(secretName)

        if (hasIconMapping) {
            iconType = mapping.icon
        }
    })

    return iconType
}


================================================
FILE: src/renderer/secrets/useCopySecretToClipboard.ts
================================================
import { ipcRenderer } from 'electron'

import Gopass from '../secrets/Gopass'
import { useNotificationContext } from '../common/notifications/NotificationProvider'

export function useCopySecretToClipboard() {
    const notificationContext = useNotificationContext()

    return (secretKey: string) => {
        Gopass.copy(secretKey)
            .then(() => {
                notificationContext.show({ status: 'OK', message: 'Secret has been copied to your clipboard.' })
                ipcRenderer.send('hideSearchWindow')
            })
            .catch(() => {
                notificationContext.show({ status: 'ERROR', message: 'Oops, something went wrong. Please try again.' })
            })
    }
}


================================================
FILE: src/renderer/types/electron-is-accelerator.d.ts
================================================
declare module 'electron-is-accelerator' {
    // tslint:disable-next-line
    function isValidElectronShortcut(shortcut: string): boolean

    namespace isValidElectronShortcut {}
    export = isValidElectronShortcut
}


================================================
FILE: src/renderer/types/fallback.d.ts
================================================
declare module 'react-materialize' {
    const x: any
    export = x
}

declare module 'react-treebeard' {
    const x: any
    export = x
}

declare module '@emotion/styled' {
    const x: any
    export = x
}

declare module 'react-keyboard-event-handler' {
    const x: any
    export = x
}

declare module 'fix-path' {
    const x: any
    export = x
}


================================================
FILE: src/renderer/types/promise-timeout.d.ts
================================================
declare module 'promise-timeout' {
    namespace promiseTimeout {
        class TimeoutError extends Error {}

        function timeout<T>(promise: Promise<T>, timeoutMillis: number): Promise<T>
    }

    export = promiseTimeout
}


================================================
FILE: src/renderer/types/string-replace-to-array.d.ts
================================================
declare module 'string-replace-to-array' {
    // tslint:disable-next-line
    function replace(haystack: string, needle: RegExp | string, newVal: string | Function): any[]

    namespace replace {}
    export = replace
}


================================================
FILE: src/shared/settings.ts
================================================
export interface UserSettings {
    secretValueLength: number
    searchShortcut: string
    showTray: boolean
    startOnLogin: boolean
}

export const DEFAULT_USER_SETTINGS: UserSettings = {
    secretValueLength: 50,
    searchShortcut: 'CmdOrCtrl+Shift+p',
    showTray: true,
    startOnLogin: true
}

export interface SystemSettings {
    environmentTestSuccessful: boolean
    releaseCheckedTimestamp?: number
    releaseCheck: {
        lastCheckTimestamp?: number
        results?: any
    }
}

export const DEFAULT_SYSTEM_SETTINGS: SystemSettings = {
    environmentTestSuccessful: false,
    releaseCheck: {}
}


================================================
FILE: test/Gopass.test.ts
================================================
import { IpcMainEvent } from 'electron'
import Gopass from '../src/renderer/secrets/Gopass'
import { ipcMain, ipcRenderer } from './mock/electron-mock'

const mockGopassResponse = (payload: string) => {
    ipcMain.once('gopass', (event: IpcMainEvent) => {
        event.sender.send('gopass-answer-1', {
            status: 'OK',
            executionId: 1,
            payload
        })
    })
    Gopass.executionId = 1
}

describe('Gopass', () => {
    const ipcRendererSendSpy = jest.spyOn(ipcRenderer, 'send')
    beforeEach(() => {
        jest.clearAllMocks()
    })

    describe('show', () => {
        it('should call Gopass correctly', async () => {
            mockGopassResponse('someValue')
            await Gopass.show('some-secret-name')

            expect(ipcRendererSendSpy).toHaveBeenCalledWith('gopass', {
                args: undefined,
                command: 'show -f "some-secret-name"',
                executionId: 1,
                pipeTextInto: undefined
            })
        })

        it('should call Gopass with escaped $,",\',` and \\ in secret names', async () => {
            mockGopassResponse('someValue')
            await Gopass.show('not nice $ " \' ` secret name"')

            expect(ipcRendererSendSpy).toHaveBeenCalledWith('gopass', {
                args: undefined,
                command: 'show -f "not nice \\$ \\" \\\' \\` secret name\\""',
                executionId: 1,
                pipeTextInto: undefined
            })
        })

        it('should deliver the secret value', async () => {
            mockGopassResponse('someValue')
            const secretValue = await Gopass.show('some-secret-name')
            expect(secretValue).toBe('someValue')
        })
    })

    describe('copy', () => {
        it('should call Gopass correctly', async () => {
            mockGopassResponse('someValue')
            await Gopass.copy('some-secret-name')

            expect(ipcRendererSendSpy).toHaveBeenCalledWith('gopass', {
                args: undefined,
                command: 'show -c "some-secret-name"',
                executionId: 1,
                pipeTextInto: undefined
            })
        })

        it('should deliver the secret value', async () => {
            mockGopassResponse('someValue')
            const secretValue = await Gopass.copy('some-secret-name')
            expect(secretValue).toBe('someValue')
        })
    })
})


================================================
FILE: test/PasswordHealthIndicator.test.ts
================================================
import { passwordStrengthColorExtractor } from '../src/renderer/explorer-app/password-health/PasswordHealthIndicator'

describe('PasswordHealthIndicator', () => {
    it.each`
        health | color
        ${70}  | ${'green'}
        ${100} | ${'green'}
        ${90}  | ${'green'}
        ${80}  | ${'green'}
        ${60}  | ${'yellow'}
        ${50}  | ${'yellow'}
        ${40}  | ${'red'}
        ${5}   | ${'red'}
        ${20}  | ${'red'}
        ${5}   | ${'red'}
    `('should result in color "$color" with health $health', ({ health, color }) => {
        expect(passwordStrengthColorExtractor(health)).toBe(color)
    })
})


================================================
FILE: test/SecretsDirectoryService.test.ts
================================================
import { Tree } from '../src/renderer/components/tree/TreeComponent'
import SecretsDirectoryService from '../src/renderer/explorer-app/side-navigation/SecretsDirectoryService'

describe('SecretsDirectoryService', () => {
    it('should transform a list of secret names into tree structure', () => {
        const secretPaths = [
            'codecentric/common/github/password',
            'codecentric/common/github/username',
            'codecentric/common/gitlab/password',
            'codecentric/common/gitlab/username',
            'codecentric/customers/some/notes'
        ]
        const tree: Tree = SecretsDirectoryService.secretPathsToTree(secretPaths, { name: 'someName', path: '' }, false)
        expect(tree).toMatchSnapshot()
    })

    it('should automatically toggle all nodes', () => {
        const secretPaths = [
            'codecentric/some-secret',
            'codecentric/common/something',
            'codecentric/common/another-thing',
            'codecentric/db/user',
            'codecentric/db/password'
        ]
        const tree: Tree = SecretsDirectoryService.secretPathsToTree(secretPaths, { name: 'someName', path: '' }, true)
        expect(tree).toMatchSnapshot()
    })

    it('should preserve previously toggled nodes when building a new tree', () => {
        const secretPaths = [
            'codecentric/some-secret',
            'codecentric/common/something',
            'codecentric/common/another-thing',
            'codecentric/db/user',
            'codecentric/db/password'
        ]
        const tree: Tree = SecretsDirectoryService.secretPathsToTree(
            secretPaths,
            {
                name: '',
                path: '',
                children: [
                    {
                        name: 'codecentric',
                        path: 'codecentric',
                        toggled: true,
                        children: [
                            { name: 'some-secret', path: 'codecentric/some-secret' },
                            { name: 'common', path: 'codecentric/common', toggled: true }
                        ]
                    }
                ]
            },
            false
        )
        expect(tree).toMatchSnapshot()
    })
})


================================================
FILE: test/SecretsFilterService.test.ts
================================================
import SecretsFilterService from '../src/renderer/explorer-app/side-navigation/SecretsFilterService'

const secretNames = [
    'codecentric/common/github/password',
    'codecentric/common/github/username',
    'codecentric/common/gitlab/password',
    'codecentric/common/gitlab/username',
    'codecentric/customers/some/notes',
    'codecentric/some-project/cassandra/dev/pw',
    'codecentric/some-project/cassandra/dev/user',
    'codecentric/some-project/cassandra/prod/pw',
    'codecentric/some-project/cassandra/prod/user'
]

describe('SecretsFilterService', () => {
    it('should do a simple search on names', () => {
        const results = SecretsFilterService.filterBySearch(secretNames, 'cassandra')
        expect(results.length).toBe(4)
    })

    it('should do a search with two terms on names', () => {
        const results = SecretsFilterService.filterBySearch(secretNames, 'cassandra user')
        expect(results.length).toBe(2)

        const moreResults = SecretsFilterService.filterBySearch(secretNames, 'cassandra user prod')
        expect(moreResults.length).toBe(1)
    })

    it('should do a search with two terms on names without matches', () => {
        const results = SecretsFilterService.filterBySearch(secretNames, 'cassandra user pp')
        expect(results.length).toBe(0)
    })
})


================================================
FILE: test/__snapshots__/SecretsDirectoryService.test.ts.snap
================================================
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`SecretsDirectoryService should automatically toggle all nodes 1`] = `
Object {
  "children": Array [
    Object {
      "children": Array [
        Object {
          "children": undefined,
          "name": "some-secret",
          "path": "codecentric/some-secret",
          "toggled": true,
        },
        Object {
          "children": Array [
            Object {
              "children": undefined,
              "name": "something",
              "path": "codecentric/common/something",
              "toggled": true,
            },
            Object {
              "children": undefined,
              "name": "another-thing",
              "path": "codecentric/common/another-thing",
              "toggled": true,
            },
          ],
          "name": "common",
          "path": "codecentric/common",
          "toggled": true,
        },
        Object {
          "children": Array [
            Object {
              "children": undefined,
              "name": "user",
              "path": "codecentric/db/user",
              "toggled": true,
            },
            Object {
              "children": undefined,
              "name": "password",
              "path": "codecentric/db/password",
              "toggled": true,
            },
          ],
          "name": "db",
          "path": "codecentric/db",
          "toggled": true,
        },
      ],
      "name": "codecentric",
      "path": "codecentric",
      "toggled": true,
    },
  ],
  "name": "Stores",
  "path": "",
  "toggled": true,
}
`;

exports[`SecretsDirectoryService should preserve previously toggled nodes when building a new tree 1`] = `
Object {
  "children": Array [
    Object {
      "children": Array [
        Object {
          "children": undefined,
          "name": "some-secret",
          "path": "codecentric/some-secret",
          "toggled": false,
        },
        Object {
          "children": Array [
            Object {
              "children": undefined,
              "name": "something",
              "path": "codecentric/common/something",
              "toggled": false,
            },
            Object {
              "children": undefined,
              "name": "another-thing",
              "path": "codecentric/common/another-thing",
              "toggled": false,
            },
          ],
          "name": "common",
          "path": "codecentric/common",
          "toggled": true,
        },
        Object {
          "children": Array [
            Object {
              "children": undefined,
              "name": "user",
              "path": "codecentric/db/user",
              "toggled": false,
            },
            Object {
              "children": undefined,
              "name": "password",
              "path": "codecentric/db/password",
              "toggled": false,
            },
          ],
          "name": "db",
          "path": "codecentric/db",
          "toggled": false,
        },
      ],
      "name": "codecentric",
      "path": "codecentric",
      "toggled": true,
    },
  ],
  "name": "Stores",
  "path": "",
  "toggled": true,
}
`;

exports[`SecretsDirectoryService should transform a list of secret names into tree structure 1`] = `
Object {
  "children": Array [
    Object {
      "children": Array [
        Object {
          "children": Array [
            Object {
              "children": Array [
                Object {
                  "children": undefined,
                  "name": "password",
                  "path": "codecentric/common/github/password",
                  "toggled": false,
                },
                Object {
                  "children": undefined,
                  "name": "username",
                  "path": "codecentric/common/github/username",
                  "toggled": false,
                },
              ],
              "name": "github",
              "path": "codecentric/common/github",
              "toggled": false,
            },
            Object {
              "children": Array [
                Object {
                  "children": undefined,
                  "name": "password",
                  "path": "codecentric/common/gitlab/password",
                  "toggled": false,
                },
                Object {
                  "children": undefined,
                  "name": "username",
                  "path": "codecentric/common/gitlab/username",
                  "toggled": false,
                },
              ],
              "name": "gitlab",
              "path": "codecentric/common/gitlab",
              "toggled": false,
            },
          ],
          "name": "common",
          "path": "codecentric/common",
          "toggled": false,
        },
        Object {
          "children": Array [
            Object {
              "children": Array [
                Object {
                  "children": undefined,
                  "name": "notes",
                  "path": "codecentric/customers/some/notes",
                  "toggled": false,
                },
              ],
              "name": "some",
              "path": "codecentric/customers/some",
              "toggled": false,
            },
          ],
          "name": "customers",
          "path": "codecentric/customers",
          "toggled": false,
        },
      ],
      "name": "codecentric",
      "path": "codecentric",
      "toggled": true,
    },
  ],
  "name": "Stores",
  "path": "",
  "toggled": true,
}
`;


================================================
FILE: test/deriveIconFromSecretName.test.ts
================================================
import { deriveIconFromSecretName } from '../src/renderer/secrets/deriveIconFromSecretName'

describe('deriveIconFromSecretName', () => {
    it('should derive "comment" icon on unclassifiable secret names', () => {
        expect(deriveIconFromSecretName('blablabla')).toBe('comment')
    })

    describe('"lock" icon', () => {
        const testCases = [
            { secretName: 'some/random/password' },
            { secretName: 'some/random/secret' },
            { secretName: 'some/random/phraseapp-key' },
            { secretName: 'some/random/passphrase' },
            { secretName: 'some/random/certificate' }
        ]

        testCases.forEach(testCase => {
            it(`should derive icon "lock" from secret name "${testCase.secretName}" which is indicating a password`, () => {
                const result = deriveIconFromSecretName(testCase.secretName)
                expect(result).toBe('lock')
            })
        })
    })

    describe('"person" icon', () => {
        const testCases = [{ secretName: 'some/random/user' }, { secretName: 'some/random/name' }, { secretName: 'some/random/id' }]

        testCases.forEach(testCase => {
            it(`should derive icon "person" from secret name "${testCase.secretName}"`, () => {
                const result = deriveIconFromSecretName(testCase.secretName)
                expect(result).toBe('person')
            })
        })
    })

    describe('"comment" icon', () => {
        const testCases = [
            { secretName: 'some/random/something-without-pattern' },
            { secretName: 'some/random/note' },
            { secretName: 'some/random/comment' },
            { secretName: 'some/random/misc' }
        ]

        testCases.forEach(testCase => {
            it(`should derive icon "comment" from secret name "${testCase.secretName}"`, () => {
                const result = deriveIconFromSecretName(testCase.secretName)
                expect(result).toBe('comment')
            })
        })
    })

    describe('"filter_center_focus" icon', () => {
        const testCases = [
            { secretName: 'some/random/uri' },
            { secretName: 'some/random/url' },
            { secretName: 'some/random/link' },
            { secretName: 'some/random/connection' }
        ]

        testCases.forEach(testCase => {
            it(`should derive icon "filter_center_focus" from secret name "${testCase.secretName}"`, () => {
                const result = deriveIconFromSecretName(testCase.secretName)
                expect(result).toBe('filter_center_focus')
            })
        })
    })
})


================================================
FILE: test/mock/electron-mock.ts
================================================
import createIPCMock from 'electron-mock-ipc'

const mocked = createIPCMock()
const ipcMain = mocked.ipcMain
const ipcRenderer = mocked.ipcRenderer

export { ipcMain, ipcRenderer }


================================================
FILE: tsconfig.json
================================================
{
    "compilerOptions": {
        "target": "es5",
        "module": "commonjs",
        "lib": [
          "dom",
          "es2015",
          "es2016",
          "es2017"
      ],
        "allowJs": true,
        "jsx": "react",
        "sourceMap": true,
        "strict": true
    }
}


================================================
FILE: tslint.json
================================================
{
    "extends": "tslint:latest",
    "jsRules": {
        "quotemark": [
            true,
            "single",
            "avoid-escape"
        ],
        "object-literal-sort-keys": false,
        "arrow-parens": false,
        "one-variable-per-declaration": [
            true,
            "ignore-for-loop"
        ],
        "semicolon": [
            true,
            "never"
        ],
        "trailing-comma": [
            true,
            {
                "multiline": "never",
                "singleline": "never"
            }
        ],
        "object-literal-key-quotes": [
            true,
            "as-needed"
        ],
        "prefer-const": true,
        "no-magic-numbers": false,
        "only-arrow-functions": [
            true,
            "allow-declarations",
            "allow-named-functions"
        ],
        "curly": true,
        "no-console": [
            true,
            "log",
            "error"
        ],
        "no-empty": true,
        "no-invalid-this": [
            true,
            "check-function-in-method"
        ],
        "no-shadowed-variable": true,
        "radix": true,
        "switch-default": true,
        "cyclomatic-complexity": [
            true,
            10
        ],
        "max-line-length": [
            true,
            160
        ]
    },
    "rules": {
        "no-implicit-dependencies": false,
        "no-submodule-imports": false,
        "quotemark": [
            true,
            "single",
            "avoid-escape"
        ],
        "object-literal-sort-keys": false,
        "arrow-parens": false,
        "one-variable-per-declaration": [
            true,
            "ignore-for-loop"
        ],
        "semicolon": [
            true,
            "never"
        ],
        "interface-name": [
            true,
            "never-prefix"
        ],
        "trailing-comma": [
            true,
            {
                "multiline": "never",
                "singleline": "never"
            }
        ],
        "object-literal-key-quotes": [
            true,
            "as-needed"
        ],
        "member-ordering": [
            true,
            {
                "order": "fields-first"
            }
        ],
        "ordered-imports": false,
        "prefer-const": true,
        "no-magic-numbers": false,
        "only-arrow-functions": [
            true,
            "allow-declarations",
            "allow-named-functions"
        ],
        "curly": true,
        "no-console": [
            true,
            "log"
        ],
        "no-empty": true,
        "no-empty-interface": false,
        "no-invalid-this": [
            true,
            "check-function-in-method"
        ],
        "no-shadowed-variable": true,
        "no-unused-expression": false,
        "no-object-literal-type-assertion": false,
        "radix": true,
        "switch-default": true,
        "cyclomatic-complexity": [
            true,
            10
        ],
        "max-line-length": [
            true,
            160
        ]
    }
}


================================================
FILE: webpack.base.config.js
================================================
'use strict'

const path = require('path')

module.exports = {
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: '[name].js'
    },
    node: {
        __dirname: false,
        __filename: false
    },
    resolve: {
        extensions: ['.tsx', '.ts', '.js', '.json']
    },
    devtool: 'source-map',
    plugins: [],
    mode: 'development'
}


================================================
FILE: webpack.main.config.js
================================================
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const CopyWebpackPlugin = require('copy-webpack-plugin')

const baseConfig = require('./webpack.base.config')

module.exports = merge.smart(baseConfig, {
    target: 'electron-main',
    entry: {
        main: './src/main/index.ts'
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                include: [path.resolve(__dirname, 'src', 'main'), path.resolve(__dirname, 'src', 'shared')],
                loader: 'awesome-typescript-loader'
            }
        ]
    },
    plugins: [
        new CopyWebpackPlugin([
            {
                from: 'src/main/assets',
                to: 'assets'
            }
        ]),
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
        })
    ]
})


================================================
FILE: webpack.main.prod.config.js
================================================
const merge = require('webpack-merge')

const baseConfig = require('./webpack.main.config')

module.exports = merge.smart(baseConfig, {
    plugins: [],
    mode: 'production'
})


================================================
FILE: webpack.renderer.explorer.config.js
================================================
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const HtmlWebpackPlugin = require('html-webpack-plugin')

const baseConfig = require('./webpack.base.config')

module.exports = merge.smart(baseConfig, {
    output: {
        path: path.resolve(__dirname, 'dist', 'explorer')
    },
    target: 'electron-renderer',
    entry: {
        app: './src/renderer/explorer-app.tsx'
    },
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                include: [path.resolve(__dirname, 'src', 'renderer')],
                loader: 'awesome-typescript-loader'
            },
            {
                test: /\.scss$/,
                loaders: ['style-loader', 'css-loader', 'sass-loader']
            },
            {
                test: /\.css$/,
                loaders: ['style-loader', 'css-loader']
            },
            {
                test: /\.(gif|png|jpe?g|svg)$/,
                use: [
                    'file-loader',
                    {
                        loader: 'image-webpack-loader',
                        options: {
                            bypassOnDebug: true
                        }
                    }
                ]
            },
            // All output '.js' files will have any sourcemaps re-processed by 'source-map-loader'.
            {
                enforce: 'pre',
                test: /\.js$/,
                loader: 'source-map-loader'
            },
            {
                test: /\.(woff(2)?|ttf|eot|svg)(\?v=\d+\.\d+\.\d+)?$/,
                use: [
                    {
                        loader: 'file-loader',
                        options: {
                            name: '[name].[ext]',
                            outputPath: 'fonts/'
                        }
                    }
                ]
            }
        ]
    },
    plugins: [
        new HtmlWebpackPlugin({
            title: 'Gopass UI'
        }),
        new webpack.DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development')
        })
    ]
})


================================================
FILE: webpack.renderer.explorer.dev.config.js
================================================
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const spawn = require('child_process').spawn

const baseConfig = require('./webpack.renderer.explorer.config')

module.exports = merge.smart(baseConfig, {
    entry: [
        'react-hot-loader/patch',
        './src/renderer/explorer-app.tsx'
    ],
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                include: [ path.resolve(__dirname, 'src', 'renderer') ],
                loaders: [ 'react-hot-loader/webpack', 'awesome-typescript-loader' ]
            }
        ]
    },
    plugins: [
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
        port: 2003,
        compress: true,
        noInfo: true,
        stats: 'errors-only',
        inline: true,
        hot: true,
        headers: { 'Access-Control-Allow-Origin': '*' },
        historyApiFallback: {
            verbose: true,
            disableDotRule: false
        },
        before() {
            if (process.env.START_HOT) {
                console.log('Starting main process');
                spawn('npm', ['run', 'start-main-dev'], {
                    shell: true,
                    env: process.env,
                    stdio: 'inherit'
                })
                    .on('close', code => process.exit(code))
                    .on('error', spawnError => console.error(spawnError));
            }
        }
    }
})


================================================
FILE: webpack.renderer.explorer.prod.config.js
================================================
const merge = require('webpack-merge')

const baseConfig = require('./webpack.renderer.explorer.config')

module.exports = merge.smart(baseConfig, {
    plugins: [],
    mode: 'production'
})


================================================
FILE: webpack.renderer.search.config.js
================================================
const merge = require('webpack-merge')
const path = require('path')

const baseConfig = require('./webpack.renderer.explorer.config')

module.exports = merge.smart(baseConfig, {
    output: {
        path: path.resolve(__dirname, 'dist', 'search'),
    },
    target: 'electron-renderer',
    entry: {
        app: './src/renderer/search-app.tsx'
    }
})


================================================
FILE: webpack.renderer.search.dev.config.js
================================================
const path = require('path')
const webpack = require('webpack')
const merge = require('webpack-merge')
const spawn = require('child_process').spawn

const baseConfig = require('./webpack.renderer.search.config')

module.exports = merge.smart(baseConfig, {
    entry: [
        'react-hot-loader/patch',
        './src/renderer/search-app.tsx'
    ],
    module: {
        rules: [
            {
                test: /\.tsx?$/,
                include: [ path.resolve(__dirname, 'src', 'renderer') ],
                loaders: [ 'react-hot-loader/webpack', 'awesome-typescript-loader' ]
            }
        ]
    },
    plugins: [
        new webpack.NamedModulesPlugin(),
        new webpack.HotModuleReplacementPlugin()
    ],
    devServer: {
        port: 2004,
        compress: true,
        noInfo: true,
        stats: 'errors-only',
        inline: true,
        hot: true,
        headers: { 'Access-Control-Allow-Origin': '*' },
        historyApiFallback: {
            verbose: true,
            disableDotRule: false
        }
    }
})


================================================
FILE: webpack.renderer.search.prod.config.js
================================================
const merge = require('webpack-merge')

const baseConfig = require('./webpack.renderer.search.config')

module.exports = merge.smart(baseConfig, {
    plugins: [],
    mode: 'production'
})
Download .txt
gitextract_1wdh2ba1/

├── .github/
│   └── workflows/
│       └── on-push.yml
├── .gitignore
├── .huskyrc
├── .nvmrc
├── .prettierrc
├── .vscode/
│   └── launch.json
├── LICENSE
├── README.md
├── docs/
│   ├── development.md
│   ├── platforms-and-packages.md
│   └── releasing.md
├── mocks/
│   ├── fileMock.js
│   └── styleMock.js
├── package.json
├── src/
│   ├── main/
│   │   ├── AppUtilities.ts
│   │   ├── AppWindows.ts
│   │   ├── GopassExecutor.ts
│   │   └── index.ts
│   ├── renderer/
│   │   ├── common/
│   │   │   ├── Settings.ts
│   │   │   └── notifications/
│   │   │       ├── Notification.tsx
│   │   │       └── NotificationProvider.tsx
│   │   ├── components/
│   │   │   ├── ExternalLink.tsx
│   │   │   ├── GoBackNavigationButton.tsx
│   │   │   ├── PaginatedTable.tsx
│   │   │   ├── RoundActionButton.tsx
│   │   │   ├── loading-screen/
│   │   │   │   ├── LoadingScreen.css
│   │   │   │   └── LoadingScreen.tsx
│   │   │   └── tree/
│   │   │       ├── TreeComponent.tsx
│   │   │       ├── TreeHeader.tsx
│   │   │       └── TreeStyle.ts
│   │   ├── explorer-app/
│   │   │   ├── ExplorerApplication.css
│   │   │   ├── ExplorerApplication.tsx
│   │   │   ├── GithubService.ts
│   │   │   ├── MainContent.tsx
│   │   │   ├── SecretsProvider.tsx
│   │   │   ├── components/
│   │   │   │   ├── EnvironmentTest.tsx
│   │   │   │   ├── LastVersionInfo.tsx
│   │   │   │   ├── MainNavigation.tsx
│   │   │   │   └── PasswordStrengthInfo.tsx
│   │   │   ├── pages/
│   │   │   │   ├── AddMountPage.tsx
│   │   │   │   ├── AddSecretPage.css
│   │   │   │   ├── AddSecretPage.tsx
│   │   │   │   ├── HomePage.tsx
│   │   │   │   ├── MountsPage.tsx
│   │   │   │   ├── PasswordHealthPage.tsx
│   │   │   │   ├── SettingsPage.tsx
│   │   │   │   └── details/
│   │   │   │       ├── HistoryTable.tsx
│   │   │   │       └── SecretDetailsPage.tsx
│   │   │   ├── password-health/
│   │   │   │   ├── PasswordHealthIndicator.tsx
│   │   │   │   ├── PasswordHealthRules.ts
│   │   │   │   ├── PasswordRater.ts
│   │   │   │   ├── PasswordRatingComponent.css
│   │   │   │   ├── PasswordRatingComponent.tsx
│   │   │   │   └── PasswordRule.d.ts
│   │   │   └── side-navigation/
│   │   │       ├── SecretExplorer.tsx
│   │   │       ├── SecretTree.tsx
│   │   │       ├── SecretsDirectoryService.ts
│   │   │       └── SecretsFilterService.ts
│   │   ├── explorer-app.tsx
│   │   ├── search-app/
│   │   │   ├── CollectionItems.tsx
│   │   │   ├── SearchApplication.css
│   │   │   ├── SearchApplication.tsx
│   │   │   ├── SearchResults.tsx
│   │   │   ├── SearchResultsView.tsx
│   │   │   └── SecretText.tsx
│   │   ├── search-app.tsx
│   │   ├── secrets/
│   │   │   ├── AsyncPasswordHealthCollector.ts
│   │   │   ├── Gopass.ts
│   │   │   ├── deriveIconFromSecretName.ts
│   │   │   └── useCopySecretToClipboard.ts
│   │   └── types/
│   │       ├── electron-is-accelerator.d.ts
│   │       ├── fallback.d.ts
│   │       ├── promise-timeout.d.ts
│   │       └── string-replace-to-array.d.ts
│   └── shared/
│       └── settings.ts
├── test/
│   ├── Gopass.test.ts
│   ├── PasswordHealthIndicator.test.ts
│   ├── SecretsDirectoryService.test.ts
│   ├── SecretsFilterService.test.ts
│   ├── __snapshots__/
│   │   └── SecretsDirectoryService.test.ts.snap
│   ├── deriveIconFromSecretName.test.ts
│   └── mock/
│       └── electron-mock.ts
├── tsconfig.json
├── tslint.json
├── webpack.base.config.js
├── webpack.main.config.js
├── webpack.main.prod.config.js
├── webpack.renderer.explorer.config.js
├── webpack.renderer.explorer.dev.config.js
├── webpack.renderer.explorer.prod.config.js
├── webpack.renderer.search.config.js
├── webpack.renderer.search.dev.config.js
└── webpack.renderer.search.prod.config.js
Download .txt
SYMBOL INDEX (129 symbols across 38 files)

FILE: src/main/GopassExecutor.ts
  type GopassOptions (line 4) | interface GopassOptions {
  class GopassExecutor (line 11) | class GopassExecutor {
    method handleEvent (line 12) | public static async handleEvent(event: IpcMainEvent, options: GopassOp...

FILE: src/renderer/common/Settings.ts
  class Settings (line 5) | class Settings {
    method getUserSettings (line 6) | public static getUserSettings(): UserSettings {
    method updateUserSettings (line 10) | public static updateUserSettings(settings: Partial<UserSettings>) {
    method getSystemSettings (line 14) | public static getSystemSettings(): SystemSettings {
    method updateSystemSettings (line 18) | public static updateSystemSettings(settings: Partial<SystemSettings>) {

FILE: src/renderer/common/notifications/Notification.tsx
  constant DEFAULT_TIMEOUT (line 7) | const DEFAULT_TIMEOUT = 3000
  function NotificationView (line 9) | function NotificationView(props: { dismissTimeout?: number }) {

FILE: src/renderer/common/notifications/NotificationProvider.tsx
  type Notification (line 3) | interface Notification {
  type NotificationContext (line 8) | interface NotificationContext {
  function useNotificationContext (line 17) | function useNotificationContext() {
  function NotificationProvider (line 27) | function NotificationProvider({ children }: any) {

FILE: src/renderer/components/ExternalLink.tsx
  function ExternalLink (line 4) | function ExternalLink(props: { url: string; children: any }) {

FILE: src/renderer/components/PaginatedTable.tsx
  type TableColumn (line 6) | interface TableColumn {
  type TableRow (line 11) | interface TableRow {
  type PaginatedTableProps (line 17) | interface PaginatedTableProps {
  type PaginatedTableState (line 22) | interface PaginatedTableState {
  class PaginatedTable (line 27) | class PaginatedTable extends React.Component<PaginatedTableProps, Pagina...
    method constructor (line 28) | constructor(props: PaginatedTableProps) {
    method render (line 36) | public render() {
    method renderPagination (line 75) | private renderPagination(pagination: PageInformation) {

FILE: src/renderer/components/RoundActionButton.tsx
  type RoundActionBtnProps (line 4) | interface RoundActionBtnProps {

FILE: src/renderer/components/loading-screen/LoadingScreen.tsx
  constant WAITING_TEXTS (line 6) | const WAITING_TEXTS = [
  function LoadingScreen (line 15) | function LoadingScreen() {

FILE: src/renderer/components/tree/TreeComponent.tsx
  type Tree (line 6) | interface Tree {
  type TreeComponentProps (line 14) | interface TreeComponentProps {
  type TreeComponentState (line 19) | interface TreeComponentState {
  class TreeComponent (line 22) | class TreeComponent extends React.Component<TreeComponentProps, TreeComp...
    method render (line 25) | public render() {

FILE: src/renderer/explorer-app/GithubService.ts
  type GithubTag (line 1) | interface GithubTag {
  class GithubService (line 6) | class GithubService {
    method getTagsOfRepository (line 7) | public static getTagsOfRepository(owner: string, repositoryName: strin...

FILE: src/renderer/explorer-app/SecretsProvider.tsx
  type SecretsContext (line 7) | interface SecretsContext {

FILE: src/renderer/explorer-app/components/EnvironmentTest.tsx
  type ErrorDetails (line 10) | type ErrorDetails = 'GOPASS_CONNECTION' | 'DECRYPTION' | undefined
  function EnvironmentTest (line 12) | function EnvironmentTest() {
  function PendingContent (line 59) | function PendingContent(props: { executeTest: () => void }) {
  function RunningContent (line 78) | function RunningContent() {
  function ErrorContent (line 89) | function ErrorContent(props: { errorDetails: ErrorDetails; reset: () => ...
  function OkContent (line 116) | function OkContent() {

FILE: src/renderer/explorer-app/components/LastVersionInfo.tsx
  constant ONE_HOUR_IN_MILLIS (line 7) | const ONE_HOUR_IN_MILLIS = 3600000
  constant VERSION_CHECK_INTERVAL (line 8) | const VERSION_CHECK_INTERVAL = ONE_HOUR_IN_MILLIS

FILE: src/renderer/explorer-app/components/MainNavigation.tsx
  type MainNavigationViewProps (line 10) | interface MainNavigationViewProps {
  function MainNavigationComponent (line 14) | function MainNavigationComponent({ history }: MainNavigationViewProps) {

FILE: src/renderer/explorer-app/components/PasswordStrengthInfo.tsx
  type PasswordStrengthInfoProps (line 4) | interface PasswordStrengthInfoProps {

FILE: src/renderer/explorer-app/pages/AddMountPage.tsx
  function AddMountPage (line 9) | function AddMountPage({ history }: RouteComponentProps) {

FILE: src/renderer/explorer-app/pages/AddSecretPage.tsx
  type AddSecretPageState (line 13) | interface AddSecretPageState {
  class AddSecretPage (line 18) | class AddSecretPage extends React.Component<RouteComponentProps, AddSecr...
    method constructor (line 19) | constructor(props: any) {
    method render (line 27) | public render() {

FILE: src/renderer/explorer-app/pages/PasswordHealthPage.tsx
  type PasswordHealthPageState (line 10) | interface PasswordHealthPageState {
  class PasswordHealthPage (line 16) | class PasswordHealthPage extends React.Component<RouteComponentProps, Pa...
    method constructor (line 17) | constructor(props: any) {
    method componentDidMount (line 24) | public async componentDidMount() {
    method componentWillUnmount (line 38) | public async componentWillUnmount() {
    method render (line 42) | public render() {
    method stopStatusChecker (line 53) | private async stopStatusChecker() {
    method renderStatus (line 63) | private renderStatus(status: PasswordHealthCollectionStatus) {
    method renderOverallPasswordHealth (line 103) | private renderOverallPasswordHealth(overallPasswordHealth: PasswordHea...
    method renderImprovementPotential (line 123) | private renderImprovementPotential(improvablePasswords: PasswordSecret...

FILE: src/renderer/explorer-app/pages/SettingsPage.tsx
  type SettingsPageState (line 9) | interface SettingsPageState {
  function SettingsPage (line 15) | function SettingsPage() {

FILE: src/renderer/explorer-app/pages/details/HistoryTable.tsx
  type HistoryTableProps (line 7) | interface HistoryTableProps {
  function HistoryTable (line 11) | function HistoryTable({ entries }: HistoryTableProps) {

FILE: src/renderer/explorer-app/pages/details/SecretDetailsPage.tsx
  type SecretDetailsPageProps (line 13) | interface SecretDetailsPageProps extends RouteComponentProps {
  constant DISPLAY_SECRET_VALUE_BY_DEFAULT (line 19) | const DISPLAY_SECRET_VALUE_BY_DEFAULT = false
  function SecretDetailsPage (line 22) | function SecretDetailsPage({ secretName, isAdded, history }: SecretDetai...

FILE: src/renderer/explorer-app/password-health/PasswordRater.ts
  type PasswordRatingResult (line 5) | interface PasswordRatingResult {
  type PasswordHealthSummary (line 14) | interface PasswordHealthSummary {
  class PasswordRater (line 19) | class PasswordRater {
    method ratePassword (line 20) | public static ratePassword(password: string): PasswordRatingResult {
    method buildOverallPasswordHealthSummary (line 31) | public static buildOverallPasswordHealthSummary(passwordHealths: Passw...

FILE: src/renderer/explorer-app/password-health/PasswordRatingComponent.tsx
  type PasswordRatingComponentProps (line 10) | interface PasswordRatingComponentProps {

FILE: src/renderer/explorer-app/password-health/PasswordRule.d.ts
  type PasswordHealthRuleInfo (line 1) | interface PasswordHealthRuleInfo {
  type PasswordHealthRule (line 6) | interface PasswordHealthRule extends PasswordHealthRuleInfo {

FILE: src/renderer/explorer-app/side-navigation/SecretTree.tsx
  type SecretTreeViewerProps (line 4) | interface SecretTreeViewerProps {
  class SecretTreeViewer (line 9) | class SecretTreeViewer extends React.Component<SecretTreeViewerProps, {}> {
    method render (line 10) | public render() {

FILE: src/renderer/explorer-app/side-navigation/SecretsDirectoryService.ts
  class SecretsDirectoryService (line 3) | class SecretsDirectoryService {
    method secretPathsToTree (line 4) | public static secretPathsToTree(secretPaths: string[], previousTree: T...
    method secretPathsToDirectory (line 14) | private static secretPathsToDirectory(secretPaths: string[]): any {
    method getToggledPathsFromTree (line 33) | private static getToggledPathsFromTree(tree: Tree): string[] {
    method directoryToTree (line 51) | private static directoryToTree(directory: any, previousTree: Tree, ope...
    method getChildren (line 64) | private static getChildren(

FILE: src/renderer/explorer-app/side-navigation/SecretsFilterService.ts
  class SecretsFilterService (line 1) | class SecretsFilterService {
    method filterBySearch (line 2) | public static filterBySearch(secretNames: string[], searchValue: strin...

FILE: src/renderer/search-app/CollectionItems.tsx
  type CollectionItemProps (line 6) | interface CollectionItemProps {
  function CollectionItems (line 13) | function CollectionItems({ filteredSecretNames, selectedItemIndex, onIte...

FILE: src/renderer/search-app/SearchApplication.tsx
  constant NAVIGATION_KEYS (line 10) | const NAVIGATION_KEYS = ['ArrowUp', 'ArrowDown', 'Enter', 'Tab']
  function SearchApplication (line 18) | function SearchApplication() {

FILE: src/renderer/search-app/SearchResults.tsx
  constant NUMBER_OF_SEARCH_RESULTS (line 10) | const NUMBER_OF_SEARCH_RESULTS = 15
  type SearchResultsProps (line 12) | interface SearchResultsProps {
  function SearchResults (line 16) | function SearchResults(props: SearchResultsProps) {
  function filterMatchingSecrets (line 106) | function filterMatchingSecrets(searchValues: string[]) {

FILE: src/renderer/search-app/SecretText.tsx
  type SecretTextProps (line 4) | interface SecretTextProps {
  function SecretText (line 19) | function SecretText({ secretPath, highlightRegExp }: SecretTextProps) {

FILE: src/renderer/secrets/AsyncPasswordHealthCollector.ts
  type PasswordSecretHealth (line 6) | interface PasswordSecretHealth {
  type PasswordHealthCollectionStatus (line 12) | interface PasswordHealthCollectionStatus {
  class AsyncPasswordHealthCollector (line 20) | class AsyncPasswordHealthCollector {
    method start (line 29) | public async start() {
    method stopAndReset (line 41) | public async stopAndReset() {
    method getCurrentStatus (line 45) | public getCurrentStatus(): PasswordHealthCollectionStatus {
    method initializePasswordSecretNames (line 50) | private async initializePasswordSecretNames(inProgress: boolean): Prom...
    method getAllSecretsContainingPasswords (line 60) | private async getAllSecretsContainingPasswords(): Promise<string[]> {

FILE: src/renderer/secrets/Gopass.ts
  type HistoryEntry (line 3) | interface HistoryEntry {
  type Mount (line 10) | interface Mount {
  class Gopass (line 19) | class Gopass {
    method copy (line 22) | public static async copy(key: string): Promise<string> {
    method show (line 26) | public static async show(key: string): Promise<string> {
    method history (line 30) | public static async history(key: string): Promise<HistoryEntry[]> {
    method getMyRecipientId (line 50) | public static async getMyRecipientId(): Promise<string | undefined> {
    method getAllMounts (line 58) | public static async getAllMounts(): Promise<Mount[]> {
    method addMount (line 76) | public static async addMount(mount: Mount) {
    method deleteMount (line 93) | public static async deleteMount(name: string) {
    method sync (line 97) | public static async sync(): Promise<void> {
    method getAllSecretNames (line 101) | public static async getAllSecretNames(): Promise<string[]> {
    method addSecret (line 107) | public static async addSecret(name: string, value: string): Promise<vo...
    method editSecret (line 111) | public static async editSecret(name: string, newValue: string): Promis...
    method deleteSecret (line 115) | public static async deleteSecret(name: string): Promise<void> {
    method execute (line 119) | private static execute(command: string, args?: string[], pipeTextInto?...

FILE: src/renderer/secrets/deriveIconFromSecretName.ts
  type SecretIconMapping (line 1) | interface SecretIconMapping {

FILE: src/renderer/secrets/useCopySecretToClipboard.ts
  function useCopySecretToClipboard (line 6) | function useCopySecretToClipboard() {

FILE: src/renderer/types/promise-timeout.d.ts
  class TimeoutError (line 3) | class TimeoutError extends Error {}

FILE: src/shared/settings.ts
  type UserSettings (line 1) | interface UserSettings {
  constant DEFAULT_USER_SETTINGS (line 8) | const DEFAULT_USER_SETTINGS: UserSettings = {
  type SystemSettings (line 15) | interface SystemSettings {
  constant DEFAULT_SYSTEM_SETTINGS (line 24) | const DEFAULT_SYSTEM_SETTINGS: SystemSettings = {

FILE: webpack.renderer.explorer.dev.config.js
  method before (line 38) | before() {
Condensed preview — 93 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (152K chars).
[
  {
    "path": ".github/workflows/on-push.yml",
    "chars": 951,
    "preview": "name: On push (tests, build)\non: push\njobs:\n  test:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout"
  },
  {
    "path": ".gitignore",
    "chars": 44,
    "preview": "node_modules/\n\n.idea/\n*.iml\n\ndist/\nrelease/\n"
  },
  {
    "path": ".huskyrc",
    "chars": 66,
    "preview": "{\n  \"hooks\": {\n    \"pre-commit\": \"npm run lint && npm test\"\n  }\n}\n"
  },
  {
    "path": ".nvmrc",
    "chars": 6,
    "preview": "14.16\n"
  },
  {
    "path": ".prettierrc",
    "chars": 202,
    "preview": "{\n    \"singleQuote\": true,\n    \"jsxSingleQuote\": true,\n    \"arrowParens\": \"avoid\",\n    \"printWidth\": 160,\n    \"tabWidth\""
  },
  {
    "path": ".vscode/launch.json",
    "chars": 434,
    "preview": "{\n    // Use IntelliSense to learn about possible attributes.\n    // Hover to view descriptions of existing attributes.\n"
  },
  {
    "path": "LICENSE",
    "chars": 1071,
    "preview": "MIT License\n\nCopyright (c) 2019 codecentric AG\n\nPermission is hereby granted, free of charge, to any person obtaining a "
  },
  {
    "path": "README.md",
    "chars": 2090,
    "preview": "# Gopass UI [![Latest release](https://img.shields.io/github/release/codecentric/gopass-ui.svg)](https://github.com/code"
  },
  {
    "path": "docs/development.md",
    "chars": 2820,
    "preview": "## Development\n\n### Clone and install dependencies\n\nFirst, clone the repository and navigate inside:\n\n```bash\ngit clone "
  },
  {
    "path": "docs/platforms-and-packages.md",
    "chars": 780,
    "preview": "## Supported Platforms\n\nGopass-ui is available for the following platforms:\n* MacOS (.dmg)\n* Windows (.exe)\n* Linux, see"
  },
  {
    "path": "docs/releasing.md",
    "chars": 1214,
    "preview": "## Releasing and Publishing Gopass UI\n\nThis documents the steps it needs to release and publish a new version of Gopass "
  },
  {
    "path": "mocks/fileMock.js",
    "chars": 34,
    "preview": "module.exports = 'test-file-stub'\n"
  },
  {
    "path": "mocks/styleMock.js",
    "chars": 20,
    "preview": "module.exports = {}\n"
  },
  {
    "path": "package.json",
    "chars": 5720,
    "preview": "{\n  \"name\": \"gopass-ui\",\n  \"version\": \"0.8.0\",\n  \"description\": \"Awesome UI for the gopass CLI – a password manager for "
  },
  {
    "path": "src/main/AppUtilities.ts",
    "chars": 792,
    "preview": "import * as electronSettings from 'electron-settings'\nimport { DEFAULT_SYSTEM_SETTINGS, DEFAULT_USER_SETTINGS, SystemSet"
  },
  {
    "path": "src/main/AppWindows.ts",
    "chars": 3153,
    "preview": "import { BrowserWindow, Menu, app, nativeTheme } from 'electron'\nimport * as url from 'url'\nimport * as path from 'path'"
  },
  {
    "path": "src/main/GopassExecutor.ts",
    "chars": 895,
    "preview": "import { exec } from 'child_process'\nimport { IpcMainEvent } from 'electron'\n\nexport interface GopassOptions {\n    execu"
  },
  {
    "path": "src/main/index.ts",
    "chars": 7474,
    "preview": "import { app, BrowserWindow, Event, globalShortcut, ipcMain, IpcMainEvent, Tray, session, shell, Accelerator } from 'ele"
  },
  {
    "path": "src/renderer/common/Settings.ts",
    "chars": 671,
    "preview": "import { ipcRenderer } from 'electron'\nimport { SystemSettings, UserSettings } from '../../shared/settings'\nimport set ="
  },
  {
    "path": "src/renderer/common/notifications/Notification.tsx",
    "chars": 1877,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { Animated } from 'react-animated-css'\n\nimp"
  },
  {
    "path": "src/renderer/common/notifications/NotificationProvider.tsx",
    "chars": 1303,
    "preview": "import * as React from 'react'\n\nexport interface Notification {\n    status: 'OK' | 'ERROR'\n    message: string\n}\n\nexport"
  },
  {
    "path": "src/renderer/components/ExternalLink.tsx",
    "chars": 298,
    "preview": "import * as React from 'react'\nimport { shell } from 'electron'\n\nexport function ExternalLink(props: { url: string; chil"
  },
  {
    "path": "src/renderer/components/GoBackNavigationButton.tsx",
    "chars": 430,
    "preview": "import * as React from 'react'\nimport { withRouter } from 'react-router'\nimport { History } from 'history'\n\nimport { Rou"
  },
  {
    "path": "src/renderer/components/PaginatedTable.tsx",
    "chars": 2816,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { paginationCalculator } from 'pagination-c"
  },
  {
    "path": "src/renderer/components/RoundActionButton.tsx",
    "chars": 363,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\nexport interface RoundActionBtnProps {\n    icon: "
  },
  {
    "path": "src/renderer/components/loading-screen/LoadingScreen.css",
    "chars": 232,
    "preview": ".loading-screen-wrapper {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    height: 500px;\n  "
  },
  {
    "path": "src/renderer/components/loading-screen/LoadingScreen.tsx",
    "chars": 935,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\nimport './LoadingScreen.css'\n\nconst WAITING_TEXTS"
  },
  {
    "path": "src/renderer/components/tree/TreeComponent.tsx",
    "chars": 1543,
    "preview": "import * as React from 'react'\nimport * as t from 'react-treebeard'\nimport { globalStyle } from './TreeStyle'\nimport { T"
  },
  {
    "path": "src/renderer/components/tree/TreeHeader.tsx",
    "chars": 749,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\nimport { deriveIconFromSecretName } from '../../s"
  },
  {
    "path": "src/renderer/components/tree/TreeStyle.ts",
    "chars": 1802,
    "preview": "const white = '#FFFFFF'\nconst none = 'none'\n\nexport const globalStyle = {\n    tree: {\n        base: {\n            listSt"
  },
  {
    "path": "src/renderer/explorer-app/ExplorerApplication.css",
    "chars": 1643,
    "preview": ".secret-explorer {\n    position: fixed;\n    z-index: 10;\n    top: 0;\n    left: 0;\n    bottom: 0;\n    width: 450px;\n    o"
  },
  {
    "path": "src/renderer/explorer-app/ExplorerApplication.tsx",
    "chars": 346,
    "preview": "import * as React from 'react'\n\nimport SecretExplorer from './side-navigation/SecretExplorer'\nimport MainContent from '."
  },
  {
    "path": "src/renderer/explorer-app/GithubService.ts",
    "chars": 907,
    "preview": "export interface GithubTag {\n    url: string\n    ref: string\n}\n\nexport default class GithubService {\n    public static g"
  },
  {
    "path": "src/renderer/explorer-app/MainContent.tsx",
    "chars": 2797,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { match, Route } from 'react-router-dom'\n\ni"
  },
  {
    "path": "src/renderer/explorer-app/SecretsProvider.tsx",
    "chars": 2171,
    "preview": "import * as React from 'react'\nimport { Tree } from '../components/tree/TreeComponent'\nimport SecretsFilterService from "
  },
  {
    "path": "src/renderer/explorer-app/components/EnvironmentTest.tsx",
    "chars": 4490,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { timeout } from 'promise-timeout'\nimport {"
  },
  {
    "path": "src/renderer/explorer-app/components/LastVersionInfo.tsx",
    "chars": 1800,
    "preview": "import * as React from 'react'\nimport { app } from '@electron/remote'\nimport GithubService, { GithubTag } from '../Githu"
  },
  {
    "path": "src/renderer/explorer-app/components/MainNavigation.tsx",
    "chars": 1706,
    "preview": "import * as React from 'react'\nimport { History } from 'history'\nimport { withRouter } from 'react-router'\n\nimport { Rou"
  },
  {
    "path": "src/renderer/explorer-app/components/PasswordStrengthInfo.tsx",
    "chars": 468,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\ninterface PasswordStrengthInfoProps {\n    strengt"
  },
  {
    "path": "src/renderer/explorer-app/pages/AddMountPage.tsx",
    "chars": 2711,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { RouteComponentProps, withRouter } from 'r"
  },
  {
    "path": "src/renderer/explorer-app/pages/AddSecretPage.css",
    "chars": 291,
    "preview": ".secret-value-textarea {\n    height: 64px;\n    font-size: 16px;\n    resize: vertical;\n    border: 1px solid #9e9e9e;\n   "
  },
  {
    "path": "src/renderer/explorer-app/pages/AddSecretPage.tsx",
    "chars": 4000,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { RouteComponentProps, withRouter } from 'r"
  },
  {
    "path": "src/renderer/explorer-app/pages/HomePage.tsx",
    "chars": 1653,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\nimport { EnvironmentTest } from '../components/En"
  },
  {
    "path": "src/renderer/explorer-app/pages/MountsPage.tsx",
    "chars": 2403,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { RouteComponentProps, withRouter } from 'r"
  },
  {
    "path": "src/renderer/explorer-app/pages/PasswordHealthPage.tsx",
    "chars": 5921,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { withRouter, RouteComponentProps } from 'r"
  },
  {
    "path": "src/renderer/explorer-app/pages/SettingsPage.tsx",
    "chars": 3186,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport * as isValidElectronShortcut from 'electron"
  },
  {
    "path": "src/renderer/explorer-app/pages/details/HistoryTable.tsx",
    "chars": 735,
    "preview": "import * as React from 'react'\nimport * as dateformat from 'dateformat'\n\nimport PaginatedTable from '../../../components"
  },
  {
    "path": "src/renderer/explorer-app/pages/details/SecretDetailsPage.tsx",
    "chars": 6068,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport { RouteComponentProps, withRouter } from 'r"
  },
  {
    "path": "src/renderer/explorer-app/password-health/PasswordHealthIndicator.tsx",
    "chars": 444,
    "preview": "import * as React from 'react'\n\nexport const passwordStrengthColorExtractor = (health: number): string => {\n    if (heal"
  },
  {
    "path": "src/renderer/explorer-app/password-health/PasswordHealthRules.ts",
    "chars": 1611,
    "preview": "import { PasswordHealthRule } from './PasswordRule'\n\nconst minimumLengthRule: PasswordHealthRule = {\n    matcher: (passw"
  },
  {
    "path": "src/renderer/explorer-app/password-health/PasswordRater.ts",
    "chars": 1506,
    "preview": "import { PasswordHealthRule, PasswordHealthRuleInfo } from './PasswordRule'\nimport { allPasswordHealthRules } from './Pa"
  },
  {
    "path": "src/renderer/explorer-app/password-health/PasswordRatingComponent.css",
    "chars": 111,
    "preview": "ol.failed-rules-list {\n    padding-left: 17px;\n}\n\nol.failed-rules-list > li > strong {\n    font-weight: 500;\n}\n"
  },
  {
    "path": "src/renderer/explorer-app/password-health/PasswordRatingComponent.tsx",
    "chars": 1944,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\nimport { PasswordRater } from './PasswordRater'\ni"
  },
  {
    "path": "src/renderer/explorer-app/password-health/PasswordRule.d.ts",
    "chars": 200,
    "preview": "export interface PasswordHealthRuleInfo {\n    name: string\n    description: string\n}\n\nexport interface PasswordHealthRul"
  },
  {
    "path": "src/renderer/explorer-app/side-navigation/SecretExplorer.tsx",
    "chars": 1338,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\nimport * as KeyboardEventHandler from 'react-keybo"
  },
  {
    "path": "src/renderer/explorer-app/side-navigation/SecretTree.tsx",
    "chars": 423,
    "preview": "import * as React from 'react'\nimport TreeComponent, { Tree } from '../../components/tree/TreeComponent'\n\nexport interfa"
  },
  {
    "path": "src/renderer/explorer-app/side-navigation/SecretsDirectoryService.ts",
    "chars": 3224,
    "preview": "import { Tree } from '../../components/tree/TreeComponent'\n\nexport default class SecretsDirectoryService {\n    public st"
  },
  {
    "path": "src/renderer/explorer-app/side-navigation/SecretsFilterService.ts",
    "chars": 631,
    "preview": "export default class SecretsFilterService {\n    public static filterBySearch(secretNames: string[], searchValue: string)"
  },
  {
    "path": "src/renderer/explorer-app.tsx",
    "chars": 753,
    "preview": "import * as React from 'react'\nimport * as ReactDOM from 'react-dom'\nimport { HashRouter as Router } from 'react-router-"
  },
  {
    "path": "src/renderer/search-app/CollectionItems.tsx",
    "chars": 981,
    "preview": "import * as m from 'react-materialize'\nimport * as React from 'react'\n\nimport { SecretText } from './SecretText'\n\nexport"
  },
  {
    "path": "src/renderer/search-app/SearchApplication.css",
    "chars": 267,
    "preview": "strong {\n    font-weight: bolder;\n}\n\n.collection .collection-item:hover {\n    background-color: #EBEBEB;\n}\n\n.collection "
  },
  {
    "path": "src/renderer/search-app/SearchApplication.tsx",
    "chars": 1393,
    "preview": "import * as React from 'react'\nimport * as m from 'react-materialize'\n\nimport SearchResults from './SearchResults'\nimpor"
  },
  {
    "path": "src/renderer/search-app/SearchResults.tsx",
    "chars": 3795,
    "preview": "import * as React from 'react'\nimport { ipcRenderer } from 'electron'\nimport * as m from 'react-materialize'\nimport * as"
  },
  {
    "path": "src/renderer/search-app/SearchResultsView.tsx",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "src/renderer/search-app/SecretText.tsx",
    "chars": 975,
    "preview": "import * as React from 'react'\nimport * as replace from 'string-replace-to-array'\n\nexport interface SecretTextProps {\n  "
  },
  {
    "path": "src/renderer/search-app.tsx",
    "chars": 491,
    "preview": "import * as React from 'react'\nimport * as ReactDOM from 'react-dom'\nimport { AppContainer } from 'react-hot-loader'\n\nim"
  },
  {
    "path": "src/renderer/secrets/AsyncPasswordHealthCollector.ts",
    "chars": 2737,
    "preview": "import Gopass from './Gopass'\nimport { sortBy } from 'lodash'\nimport { passwordSecretRegex } from './deriveIconFromSecre"
  },
  {
    "path": "src/renderer/secrets/Gopass.ts",
    "chars": 4579,
    "preview": "import { ipcRenderer } from 'electron'\n\nexport interface HistoryEntry {\n    hash: string\n    author: string\n    timestam"
  },
  {
    "path": "src/renderer/secrets/deriveIconFromSecretName.ts",
    "chars": 864,
    "preview": "export interface SecretIconMapping {\n    regex: RegExp\n    icon: string\n}\n\nexport const passwordSecretRegex: RegExp = /("
  },
  {
    "path": "src/renderer/secrets/useCopySecretToClipboard.ts",
    "chars": 713,
    "preview": "import { ipcRenderer } from 'electron'\n\nimport Gopass from '../secrets/Gopass'\nimport { useNotificationContext } from '."
  },
  {
    "path": "src/renderer/types/electron-is-accelerator.d.ts",
    "chars": 220,
    "preview": "declare module 'electron-is-accelerator' {\n    // tslint:disable-next-line\n    function isValidElectronShortcut(shortcut"
  },
  {
    "path": "src/renderer/types/fallback.d.ts",
    "chars": 357,
    "preview": "declare module 'react-materialize' {\n    const x: any\n    export = x\n}\n\ndeclare module 'react-treebeard' {\n    const x: "
  },
  {
    "path": "src/renderer/types/promise-timeout.d.ts",
    "chars": 232,
    "preview": "declare module 'promise-timeout' {\n    namespace promiseTimeout {\n        class TimeoutError extends Error {}\n\n        f"
  },
  {
    "path": "src/renderer/types/string-replace-to-array.d.ts",
    "chars": 222,
    "preview": "declare module 'string-replace-to-array' {\n    // tslint:disable-next-line\n    function replace(haystack: string, needle"
  },
  {
    "path": "src/shared/settings.ts",
    "chars": 622,
    "preview": "export interface UserSettings {\n    secretValueLength: number\n    searchShortcut: string\n    showTray: boolean\n    start"
  },
  {
    "path": "test/Gopass.test.ts",
    "chars": 2428,
    "preview": "import { IpcMainEvent } from 'electron'\nimport Gopass from '../src/renderer/secrets/Gopass'\nimport { ipcMain, ipcRendere"
  },
  {
    "path": "test/PasswordHealthIndicator.test.ts",
    "chars": 636,
    "preview": "import { passwordStrengthColorExtractor } from '../src/renderer/explorer-app/password-health/PasswordHealthIndicator'\n\nd"
  },
  {
    "path": "test/SecretsDirectoryService.test.ts",
    "chars": 2259,
    "preview": "import { Tree } from '../src/renderer/components/tree/TreeComponent'\nimport SecretsDirectoryService from '../src/rendere"
  },
  {
    "path": "test/SecretsFilterService.test.ts",
    "chars": 1326,
    "preview": "import SecretsFilterService from '../src/renderer/explorer-app/side-navigation/SecretsFilterService'\n\nconst secretNames "
  },
  {
    "path": "test/__snapshots__/SecretsDirectoryService.test.ts.snap",
    "chars": 5584,
    "preview": "// Jest Snapshot v1, https://goo.gl/fbAQLP\n\nexports[`SecretsDirectoryService should automatically toggle all nodes 1`] ="
  },
  {
    "path": "test/deriveIconFromSecretName.test.ts",
    "chars": 2615,
    "preview": "import { deriveIconFromSecretName } from '../src/renderer/secrets/deriveIconFromSecretName'\n\ndescribe('deriveIconFromSec"
  },
  {
    "path": "test/mock/electron-mock.ts",
    "chars": 181,
    "preview": "import createIPCMock from 'electron-mock-ipc'\n\nconst mocked = createIPCMock()\nconst ipcMain = mocked.ipcMain\nconst ipcRe"
  },
  {
    "path": "tsconfig.json",
    "chars": 291,
    "preview": "{\n    \"compilerOptions\": {\n        \"target\": \"es5\",\n        \"module\": \"commonjs\",\n        \"lib\": [\n          \"dom\",\n    "
  },
  {
    "path": "tslint.json",
    "chars": 3077,
    "preview": "{\n    \"extends\": \"tslint:latest\",\n    \"jsRules\": {\n        \"quotemark\": [\n            true,\n            \"single\",\n      "
  },
  {
    "path": "webpack.base.config.js",
    "chars": 376,
    "preview": "'use strict'\n\nconst path = require('path')\n\nmodule.exports = {\n    output: {\n        path: path.resolve(__dirname, 'dist"
  },
  {
    "path": "webpack.main.config.js",
    "chars": 914,
    "preview": "const path = require('path')\nconst webpack = require('webpack')\nconst merge = require('webpack-merge')\nconst CopyWebpack"
  },
  {
    "path": "webpack.main.prod.config.js",
    "chars": 179,
    "preview": "const merge = require('webpack-merge')\n\nconst baseConfig = require('./webpack.main.config')\n\nmodule.exports = merge.smar"
  },
  {
    "path": "webpack.renderer.explorer.config.js",
    "chars": 2143,
    "preview": "const path = require('path')\nconst webpack = require('webpack')\nconst merge = require('webpack-merge')\nconst HtmlWebpack"
  },
  {
    "path": "webpack.renderer.explorer.dev.config.js",
    "chars": 1515,
    "preview": "const path = require('path')\nconst webpack = require('webpack')\nconst merge = require('webpack-merge')\nconst spawn = req"
  },
  {
    "path": "webpack.renderer.explorer.prod.config.js",
    "chars": 192,
    "preview": "const merge = require('webpack-merge')\n\nconst baseConfig = require('./webpack.renderer.explorer.config')\n\nmodule.exports"
  },
  {
    "path": "webpack.renderer.search.config.js",
    "chars": 356,
    "preview": "const merge = require('webpack-merge')\nconst path = require('path')\n\nconst baseConfig = require('./webpack.renderer.expl"
  },
  {
    "path": "webpack.renderer.search.dev.config.js",
    "chars": 1051,
    "preview": "const path = require('path')\nconst webpack = require('webpack')\nconst merge = require('webpack-merge')\nconst spawn = req"
  },
  {
    "path": "webpack.renderer.search.prod.config.js",
    "chars": 190,
    "preview": "const merge = require('webpack-merge')\n\nconst baseConfig = require('./webpack.renderer.search.config')\n\nmodule.exports ="
  }
]

About this extraction

This page contains the full source code of the codecentric/gopass-ui GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 93 files (136.7 KB), approximately 33.7k tokens, and a symbol index with 129 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!