Full Code of oldj/SwitchHosts for AI

master 8f9941a0e0c3 cached
248 files
468.2 KB
138.3k tokens
325 symbols
1 requests
Download .txt
Showing preview only (523K chars total). Download the full file or copy to clipboard to get everything.
Repository: oldj/SwitchHosts
Branch: master
Commit: 8f9941a0e0c3
Files: 248
Total size: 468.2 KB

Directory structure:
gitextract_xbk83j9u/

├── .gitattributes
├── .github/
│   └── issue_template.md
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .vscode/
│   └── settings.json
├── LICENSE
├── README.md
├── README.pl.md
├── README.zh_hans.md
├── README.zh_hant.md
├── alfred/
│   ├── Readme.txt
│   └── info.plist
├── app/
│   └── package.json
├── assets/
│   └── app.icns
├── package.json
├── scripts/
│   ├── entitlements.mac.plist
│   ├── hooks/
│   │   ├── artifactBuildCompleted.mjs
│   │   └── notarize-options.mjs
│   ├── libs/
│   │   ├── build-env.mjs
│   │   ├── build-log.mjs
│   │   ├── build-plan.mjs
│   │   ├── build-state.mjs
│   │   └── my-exec.mjs
│   ├── make.mjs
│   ├── release-config.mjs
│   ├── upload-diagnostics.mjs
│   ├── upload-progress.mjs
│   ├── upload-release.mjs
│   ├── vars.mjs
│   └── version-up.mjs
├── src/
│   ├── common/
│   │   ├── acknowledgements.ts
│   │   ├── constants.ts
│   │   ├── data.d.ts
│   │   ├── default_configs.ts
│   │   ├── events.ts
│   │   ├── hostsFn.ts
│   │   ├── i18n/
│   │   │   ├── index.ts
│   │   │   └── languages/
│   │   │       ├── de.ts
│   │   │       ├── en.ts
│   │   │       ├── fr.ts
│   │   │       ├── ja.ts
│   │   │       ├── ko.ts
│   │   │       ├── pl.ts
│   │   │       ├── tr.ts
│   │   │       ├── zh-hant.ts
│   │   │       └── zh.ts
│   │   ├── newlines.ts
│   │   ├── normalize.ts
│   │   ├── tree.ts
│   │   ├── types.d.ts
│   │   ├── update.ts
│   │   └── utils/
│   │       └── wait.ts
│   ├── main/
│   │   ├── actions/
│   │   │   ├── checkUpdate.ts
│   │   │   ├── closeMainWindow.ts
│   │   │   ├── cmd/
│   │   │   │   ├── changeDataDir.ts
│   │   │   │   ├── clearHistory.ts
│   │   │   │   ├── deleteHistory.ts
│   │   │   │   ├── focusMainWindow.ts
│   │   │   │   ├── getHistoryList.ts
│   │   │   │   ├── toggleDevTools.ts
│   │   │   │   └── tryToRun.ts
│   │   │   ├── config/
│   │   │   │   ├── all.ts
│   │   │   │   ├── get.ts
│   │   │   │   ├── set.ts
│   │   │   │   └── update.ts
│   │   │   ├── downloadUpdate.ts
│   │   │   ├── find/
│   │   │   │   ├── addHistory.ts
│   │   │   │   ├── addReplaceHistory.ts
│   │   │   │   ├── findBy.ts
│   │   │   │   ├── findPositionsInContent.ts
│   │   │   │   ├── getHistory.ts
│   │   │   │   ├── getReplaceHistory.ts
│   │   │   │   ├── setHistory.ts
│   │   │   │   ├── setReplaceHistory.ts
│   │   │   │   ├── show.ts
│   │   │   │   └── splitContent.ts
│   │   │   ├── getBasicData.ts
│   │   │   ├── getDataDir.ts
│   │   │   ├── getDefaultDataDir.ts
│   │   │   ├── hosts/
│   │   │   │   ├── deleteHistory.ts
│   │   │   │   ├── getContent.ts
│   │   │   │   ├── getHistoryList.ts
│   │   │   │   ├── getPathOfSystemHostsPath.ts
│   │   │   │   ├── getSystemHosts.ts
│   │   │   │   ├── refresh.ts
│   │   │   │   ├── setContent.ts
│   │   │   │   └── setSystemHosts.ts
│   │   │   ├── index.ts
│   │   │   ├── installUpdate.ts
│   │   │   ├── list/
│   │   │   │   ├── getContentOfList.ts
│   │   │   │   ├── getItem.ts
│   │   │   │   ├── getList.ts
│   │   │   │   ├── moveItemToTrashcan.ts
│   │   │   │   ├── moveManyToTrashcan.ts
│   │   │   │   └── setList.ts
│   │   │   ├── migrate/
│   │   │   │   ├── checkIfMigration.ts
│   │   │   │   ├── export.ts
│   │   │   │   ├── import.ts
│   │   │   │   ├── importFromUrl.ts
│   │   │   │   ├── importV3Data.ts
│   │   │   │   └── migrateData.ts
│   │   │   ├── openUrl.ts
│   │   │   ├── ping.ts
│   │   │   ├── quit.ts
│   │   │   ├── showItemInFolder.ts
│   │   │   ├── trashcan/
│   │   │   │   ├── clear.ts
│   │   │   │   ├── deleteItem.ts
│   │   │   │   ├── getList.ts
│   │   │   │   └── restoreItem.ts
│   │   │   └── updateTrayTitle.ts
│   │   ├── core/
│   │   │   ├── agent.ts
│   │   │   ├── getI18N.ts
│   │   │   ├── message.ts
│   │   │   ├── popupMenu.ts
│   │   │   └── updater.ts
│   │   ├── data/
│   │   │   └── index.ts
│   │   ├── http/
│   │   │   ├── api/
│   │   │   │   ├── index.ts
│   │   │   │   ├── list.ts
│   │   │   │   └── toggle.ts
│   │   │   └── index.ts
│   │   ├── libs/
│   │   │   ├── cron.ts
│   │   │   ├── getConfigDir.ts
│   │   │   ├── getDataDir.ts
│   │   │   ├── getIndex.ts
│   │   │   ├── isDev.ts
│   │   │   ├── request.ts
│   │   │   ├── safePSWD.ts
│   │   │   └── tracer.ts
│   │   ├── main.ts
│   │   ├── preload.ts
│   │   ├── types.d.ts
│   │   ├── ui/
│   │   │   ├── checkSystemLocale.ts
│   │   │   ├── find.ts
│   │   │   ├── menu.ts
│   │   │   └── tray/
│   │   │       ├── index.ts
│   │   │       └── window.ts
│   │   └── utils/
│   │       └── fs2.ts
│   ├── renderer/
│   │   ├── common/
│   │   │   └── PageWrapper.tsx
│   │   ├── components/
│   │   │   ├── About/
│   │   │   │   ├── AboutContent.module.scss
│   │   │   │   ├── AboutContent.tsx
│   │   │   │   ├── index.module.scss
│   │   │   │   └── index.tsx
│   │   │   ├── BrowserLink.tsx
│   │   │   ├── EditHostsInfo.module.scss
│   │   │   ├── EditHostsInfo.tsx
│   │   │   ├── Editor/
│   │   │   │   ├── HostsEditor.module.scss
│   │   │   │   ├── HostsEditor.tsx
│   │   │   │   ├── hosts_highlight.test.ts
│   │   │   │   └── hosts_highlight.ts
│   │   │   ├── History.module.scss
│   │   │   ├── History.tsx
│   │   │   ├── HostsViewer.module.scss
│   │   │   ├── HostsViewer.tsx
│   │   │   ├── ItemIcon.tsx
│   │   │   ├── Lang.tsx
│   │   │   ├── LeftPanel/
│   │   │   │   ├── SystemHostsItem.module.scss
│   │   │   │   ├── SystemHostsItem.tsx
│   │   │   │   ├── Trashcan.module.scss
│   │   │   │   ├── Trashcan.tsx
│   │   │   │   ├── TrashcanItem.module.scss
│   │   │   │   ├── TrashcanItem.tsx
│   │   │   │   ├── index.module.scss
│   │   │   │   └── index.tsx
│   │   │   ├── List/
│   │   │   │   ├── ListItem.module.scss
│   │   │   │   ├── ListItem.tsx
│   │   │   │   ├── index.module.scss
│   │   │   │   └── index.tsx
│   │   │   ├── Loading.module.scss
│   │   │   ├── Loading.tsx
│   │   │   ├── MainPanel/
│   │   │   │   ├── index.module.scss
│   │   │   │   └── index.tsx
│   │   │   ├── Pref/
│   │   │   │   ├── Advanced.tsx
│   │   │   │   ├── Commands.tsx
│   │   │   │   ├── CommandsHistory.tsx
│   │   │   │   ├── General.tsx
│   │   │   │   ├── Proxy.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   └── styles.module.scss
│   │   │   ├── SetWriteMode.module.scss
│   │   │   ├── SetWriteMode.tsx
│   │   │   ├── SideDrawer.tsx
│   │   │   ├── StatusBar.module.scss
│   │   │   ├── StatusBar.tsx
│   │   │   ├── SudoPasswordInput.module.scss
│   │   │   ├── SudoPasswordInput.tsx
│   │   │   ├── SwitchButton.module.scss
│   │   │   ├── SwitchButton.tsx
│   │   │   ├── TopBar/
│   │   │   │   ├── ConfigMenu.module.scss
│   │   │   │   ├── ConfigMenu.tsx
│   │   │   │   ├── ImportFromUrl.module.scss
│   │   │   │   ├── ImportFromUrl.tsx
│   │   │   │   ├── index.module.scss
│   │   │   │   └── index.tsx
│   │   │   ├── Transfer.module.scss
│   │   │   ├── Transfer.tsx
│   │   │   ├── Tree/
│   │   │   │   ├── Node.tsx
│   │   │   │   ├── Tree.tsx
│   │   │   │   ├── fn.ts
│   │   │   │   ├── index.tsx
│   │   │   │   └── style.module.scss
│   │   │   └── UpdateDialog.tsx
│   │   ├── core/
│   │   │   ├── PopupMenu.ts
│   │   │   ├── agent.ts
│   │   │   └── useOnBroadcast.ts
│   │   ├── index.html
│   │   ├── index.tsx
│   │   ├── models/
│   │   │   ├── useConfigs.ts
│   │   │   ├── useHostsData.ts
│   │   │   └── useI18n.ts
│   │   ├── pages/
│   │   │   ├── find.module.scss
│   │   │   ├── find.tsx
│   │   │   ├── index.module.scss
│   │   │   ├── index.tsx
│   │   │   ├── tray.module.scss
│   │   │   └── tray.tsx
│   │   ├── stores/
│   │   │   ├── configs.ts
│   │   │   ├── hosts_data.ts
│   │   │   └── i18n.ts
│   │   ├── styles/
│   │   │   ├── common.scss
│   │   │   ├── fn.scss
│   │   │   ├── global.scss
│   │   │   ├── scrollbar.scss
│   │   │   ├── themes/
│   │   │   │   ├── dark.scss
│   │   │   │   └── light.scss
│   │   │   └── var.scss
│   │   └── utils/
│   │       └── css-var.ts
│   └── version.json
├── test/
│   ├── _base.ts
│   ├── common/
│   │   ├── hostsFn.test.ts
│   │   ├── mock/
│   │   │   ├── normalize.001.input.hosts
│   │   │   └── normalize.001.output.hosts
│   │   ├── newlines.test.ts
│   │   └── normalize.test.ts
│   ├── main/
│   │   ├── basic.test.ts
│   │   ├── findInContent.test.ts
│   │   ├── http.test.ts
│   │   ├── setSystemHosts.test.ts
│   │   ├── splitContent.test.ts
│   │   └── trashcan.test.ts
│   ├── scripts/
│   │   ├── upload-diagnostics.test.ts
│   │   └── upload-progress.test.ts
│   └── setup.ts
├── tsconfig.json
├── typings.d.ts
├── vite.main.config.mts
├── vite.render.config.mts
└── vitest.config.mts

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

================================================
FILE: .gitattributes
================================================
legacy/* linguist-vendored
scripts/alfred/workflow/* linguist-vendored

*.css linguist-language=javascript


================================================
FILE: .github/issue_template.md
================================================
### System (Mac, Windows 10/11, Linux) / 操作系统



### SwitchHosts Version / SwitchHosts 版本



### Description / 描述



### How to reproduce / 重现步骤




================================================
FILE: .gitignore
================================================
.idea

# dependencies
node_modules
npm-debug.log*
yarn-error.log
yarn.lock
package-lock.json

# production
build
dist

# misc
.DS_Store

# umi
src/.umi
src/.umi-production
src/.umi-test
src/renderer/.umi
src/renderer/.umi-production
src/renderer/.umi-test
src/renderer/dist
.env.local

tmp
test/tmp

.env
*.provisionprofile


================================================
FILE: .prettierignore
================================================
src/renderer/.umi
src/renderer/.umi-production


================================================
FILE: .prettierrc.json
================================================
{
  "arrowParens": "always",
  "bracketSpacing": true,
  "embeddedLanguageFormatting": "auto",
  "endOfLine": "lf",
  "htmlWhitespaceSensitivity": "css",
  "insertPragma": false,
  "jsxBracketSameLine": false,
  "jsxSingleQuote": false,
  "printWidth": 100,
  "proseWrap": "preserve",
  "quoteProps": "as-needed",
  "requirePragma": false,
  "semi": false,
  "singleQuote": true,
  "tabWidth": 2,
  "trailingComma": "all",
  "useTabs": false
}


================================================
FILE: .vscode/settings.json
================================================
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.organizeImports": "explicit"
  },
  "files.exclude": {
    "**/.idea": true,
    "**/build": true,
    "**/dist": true,
    "**/node_modules": true
  },
  "search.exclude": {
    "**/.idea": true,
    "**/build": true,
    "**/dist": true,
    "**/node_modules": true
  },
  "files.watcherExclude": {
    "**/.idea/**": true,
    "**/build/**": true,
    "**/dist/**": true,
    "**/node_modules/**": true
  }
}


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright 2011-2025 oldj

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
<div align="center" markdown="1">
  <sup>Special thanks to:</sup>
  <br>
  <a href="https://go.warp.dev/SwitchHosts">
    <img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/bb4a0222-12bf-4c79-bb80-a8ed4672b801" />
  </a>

### [Warp, the intelligent terminal for developers](https://go.warp.dev/SwitchHosts)

[Available for MacOS, Linux, & Windows](https://go.warp.dev/SwitchHosts)<br>

</div>

---

# SwitchHosts

- [Polski](README.pl.md)
- [简体中文](README.zh_hans.md)
- [繁體中文](README.zh_hant.md)

Homepage: [https://switchhosts.vercel.app](https://switchhosts.vercel.app)

SwitchHosts is an App for managing hosts file, it is based on [Electron](http://electron.atom.io/), [React](https://facebook.github.io/react/), [Jotai](https://jotai.org/), [Mantine](https://mantine.dev/), etc.

## Screenshot

<img src="https://raw.githubusercontent.com/oldj/SwitchHosts/master/screenshots/sh_light.png" alt="Capture" width="960">

## Features

- Switch hosts quickly
- Syntax highlight
- Remote hosts
- Switch from system tray

## Install

### Download

You can download the source code and build it yourself, or download the built version from following
links:

- [SwitchHosts Download Page (GitHub release)](https://github.com/oldj/SwitchHosts/releases)

You can also install the built version using the [package manager Chocolatey](https://community.chocolatey.org/packages/switchhosts):

```powershell
choco install switchhosts
```

## Backup

SwitchHosts stores data at `~/.SwitchHosts` (Or folder `.SwitchHosts` under the current user's home
path on Windows), the `~/.SwitchHosts/data` folder contains data, while the `~/.SwitchHosts/config`
folder contains various configuration information.

## Develop and build

### Development

- Install [Node.js](https://nodejs.org/)
- Change to the folder `./`, run `npm install` to install dependented libraries
- Run `npm run dev` to start the development server
- Then run `npm run start` to start the app for developing or debuging

### Build and package

- It is recommended to use [electron-builder](https://github.com/electron-userland/electron-builder)
  for packaging
- Go to the `./` folder
- Run `npm run build`
- Run `npm run make`, if everything goes well, the packaged files will be in the `./dist` folder.
- This command may take several minutes to finish when you run it the first time, as it needs time
  to download dependent files. You can download the dependencies
  manually [here](https://github.com/electron/electron/releases),
  or [Taobao mirror](https://npmmirror.com/mirrors/electron/), then save the files to `~/.electron`
  . You can check the [Electron Docs](http://electron.atom.io/docs/) for more infomation.

```bash
# build
npm run build

# make
npm run make # the packed files will be in ./dist
```

## Copyright

SwitchHosts is a free and open source software, it is released under the [Apache License](./LICENSE).


================================================
FILE: README.pl.md
================================================
<div align="center" markdown="1">
  <sup>Special thanks to:</sup>
  <br>
  <a href="https://go.warp.dev/SwitchHosts">
    <img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/bb4a0222-12bf-4c79-bb80-a8ed4672b801" />
  </a>

### [Warp, the intelligent terminal for developers](https://go.warp.dev/SwitchHosts)
[Available for MacOS, Linux, & Windows](https://go.warp.dev/SwitchHosts)<br>

</div>

---

# SwitchHosts

- [English](README.md)
- [简体中文](README.zh_hans.md)
- [繁體中文](README.zh_hant.md)

Strona główna: [https://switchhosts.vercel.app](https://switchhosts.vercel.app)

SwitchHosts to aplikacja do zarządzania plikiem hosts, zbudowana na bazie [Electron](http://electron.atom.io/), [React](https://facebook.github.io/react/), [Jotai](https://jotai.org/), [Mantine](https://mantine.dev/) i innych.

## Zrzut ekranu

<img src="https://raw.githubusercontent.com/oldj/SwitchHosts/master/screenshots/sh_light.png" alt="Zrzut aplikacji" width="960">

## Funkcje

- Szybkie przełączanie hostów
- Podświetlanie składni
- Hosty zdalne
- Przełączanie z paska systemowego

## Instalacja

### Pobieranie

Możesz pobrać kod źródłowy i zbudować go samodzielnie, lub pobrać wbudowaną wersję z poniższych linków:

- [Pobierz najnowszą wersję SwitchHosts (GitHub release)](https://github.com/oldj/SwitchHosts/releases)

Możesz także zainstalować build używając [menedżera pakietów Chocolatey](https://community.chocolatey.org/packages/switchhosts):
```powershell
choco install switchhosts
```

## Kopia zapasowa

SwitchHosts przechowuje dane w `~/.SwitchHosts` (lub folder `.SwitchHosts` w ścieżce domowej bieżącego użytkownika na Windows), folder `~/.SwitchHosts/data` zawiera dane, podczas gdy folder `~/.SwitchHosts/config` zawiera różne informacje konfiguracyjne.

## Tworzenie i budowanie

### Tworzenie

- Zainstaluj [Node.js](https://nodejs.org/)
- Przejdź do folderu `./`, uruchom `npm install` aby zainstalować biblioteki zależności
- Uruchom `npm run dev` aby uruchomić serwer deweloperski
- Następnie uruchom `npm run start` aby uruchomić aplikację do tworzenia lub debugowania

### Budowanie i pakowanie

- Zaleca się użycie [electron-builder](https://github.com/electron-userland/electron-builder) do budowania
- Przejdź do folderu `./`
- Uruchom `npm run build`
- Uruchom `npm run make`, jeśli wszystko pójdzie dobrze, spakowane pliki będą w folderze `./dist`.
- Ta komenda może zająć kilka minut gdy uruchamiasz ją po raz pierwszy, ponieważ potrzebuje czasu na pobranie plików zależności. Możesz pobrać zależności ręcznie [tutaj](https://github.com/electron/electron/releases), lub [lustro Taobao](https://npmmirror.com/mirrors/electron/), a następnie zapisz pliki do `~/.electron`. Możesz sprawdzić [Dokumentację Electron](http://electron.atom.io/docs/) aby uzyskać więcej informacji.

```bash
# budowanie
npm run build

# pakowanie
npm run make # spakowane pliki będą w ./dist
```

## Prawa autorskie

SwitchHosts to wolne i otwarte oprogramowanie, wydane na licencji [Apache License](./LICENSE).


================================================
FILE: README.zh_hans.md
================================================
<div align="center" markdown="1">
  <sup>Special thanks to:</sup>
  <br>
  <a href="https://go.warp.dev/SwitchHosts">
    <img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/352a755a-6776-43fd-b324-19dc649747b2" />
  </a>

### [Warp, the intelligent terminal for developers](https://go.warp.dev/SwitchHosts)
[Available for MacOS, Linux, & Windows](https://go.warp.dev/SwitchHosts)<br>

</div>

---

# SwitchHosts

- [English](README.md)
- [Polski](README.pl.md)
- [繁體中文](README.zh_hant.md)

项目主页:[https://switchhosts.vercel.app](https://switchhosts.vercel.app)

SwitchHosts 是一个管理 hosts 文件的应用,基于 [Electron](http://electron.atom.io/)、[React](https://facebook.github.io/react/)、[Jotai](https://jotai.org/)、[Mantine](https://mantine.dev/) 等技术开发。

## 截图

<img src="https://raw.githubusercontent.com/oldj/SwitchHosts/master/screenshots/sh_light.png" alt="Capture" width="960">

## 功能特性

- 快速切换 hosts 方案
- hosts 语法高亮
- 支持从网络加载远程 hosts 配置
- 可从系统菜单栏图标快速切换 hosts

## 安装

### 下载

你可以下载源码并自行构建,也可以从以下地址下载已构建好的版本:

- [SwitchHosts Download Page (GitHub release)](https://github.com/oldj/SwitchHosts/releases)

你也可以通过 [Chocolatey 包管理器](https://community.chocolatey.org/packages/switchhosts)安装已构建好的版本:
```powershell
choco install switchhosts
```

## 数据备份

SwitchHosts 的数据文件存储于 `~/.SwitchHosts` (Windows 下存储于用户个人文件夹下的 `.SwitchHosts` 文件夹),
其中 `~/.SwitchHosts/data` 文件夹包含数据,`~/.SwitchHosts/config` 文件夹包含各项配置信息。

## 开发以及构建

### 开发

- 安装 [Node.js](https://nodejs.org/)
- 在项目根目录 `./` 下,运行 `npm install` 命令安装依赖
- 运行 `npm run dev` 命令启动开发服务
- 运行 `npm run start` 启动 App,即可开始开发及调试

### 构建及打包

- 推荐使用 [electron-builder](https://github.com/electron-userland/electron-builder) 进行打包
- 转到项目根目录 './'
- 运行 `npm run build`
- 运行 `npm run make`,如果一切顺利,可在 `./dist` 目录下找到打包后的文件
- 首次运行可能需要花费一些时间,因为需要下载相关依赖文件。你也可以从 [这儿](https://github.com/electron/electron/releases)
  或者 [淘宝镜像](https://npmmirror.com/mirrors/electron/) 手动下载,并保存到 `~/.electron`
  目录下。更多信息可访问 [Electron 文档](http://electron.atom.io/docs/)。

```bash
# build
npm run build

# make
npm run make # the packed files will be in ./dist
```

## 版权

SwitchHosts 是一个免费开源软件,基于 Apache-2.0 协议发布。


================================================
FILE: README.zh_hant.md
================================================
<div align="center" markdown="1">
  <sup>Special thanks to:</sup>
  <br>
  <a href="https://go.warp.dev/SwitchHosts">
    <img alt="Warp sponsorship" width="400" src="https://github.com/user-attachments/assets/352a755a-6776-43fd-b324-19dc649747b2" />
  </a>

### [Warp, the intelligent terminal for developers](https://go.warp.dev/SwitchHosts)

[Available for MacOS, Linux, & Windows](https://go.warp.dev/SwitchHosts)<br>

</div>

---

# SwitchHosts

- [English](README.md)
- [Polski](README.pl.md)
- [简体中文](README.zh_hans.md)

項目主頁:[https://switchhosts.vercel.app](https://switchhosts.vercel.app)

SwitchHosts 是一個管理 hosts 檔案的應用程式,基於 [Electron](http://electron.atom.io/)、[React](https://facebook.github.io/react/)、[Jotai](https://jotai.org/)、[Mantine](https://mantine.dev/) 等技術開發。

## 螢幕截圖

<img src="https://raw.githubusercontent.com/oldj/SwitchHosts/master/screenshots/sh_light.png" alt="Capture" width="960">

## 功能特性

- 快速切換 hosts 方案
- hosts 語法高亮顯示
- 支援從網路載入遠程 hosts 設定
- 可從系統菜單欄圖是快速切換 hosts

## 安裝

### 下載

你可以下載原始碼並自行建置,也可以從以下網址下載已經建置好的版本:

- [SwitchHosts Download Page (GitHub release)](https://github.com/oldj/SwitchHosts/releases)

你也可以通過 [Chocolatey 包管理器](https://community.chocolatey.org/packages/switchhosts)安裝已經建置好的版本:

```powershell
choco install switchhosts
```

## 數據備份

SwitchHosts 的數據文件儲存於 `~/.SwitchHosts` (Windows 下儲存使用者個人文件裡的 `.SwitchHosts` 資料夾),
其中 `~/.SwitchHosts/data` 資料夾包含數據,`~/.SwitchHosts/config` 資料夾包含各種設定。

## 開發及建置

### 開發

- 安裝 [Node.js](https://nodejs.org/)
- 在項目根目錄 `./` 下,執行 `npm install` 指令安裝前置
- 執行 `npm run dev` 指令啟動開發服務
- 執行 `npm run start` 啟動應用程式,即可開始開發及測試

### 打包

- 推薦使用 [electron-builder](https://github.com/electron-userland/electron-builder) 進行打包
- 轉到項目根目錄 './'
- 執行 `npm run build`
- 執行 `npm run make`,如果一切順利,可在 `./dist` 目錄下找到打包後的檔案
- 首次執行可能需要花費一點時間,因為需要下載相關的前置檔案。你也可以從 [這裡](https://github.com/electron/electron/releases)
  手動下載,並儲存到 `~/.electron`目錄下。更多資訊可以參考 [Electron 文檔](http://electron.atom.io/docs/)。

```bash
# build
npm run build
# make
npm run make # the packed files will be in ./dist
```

## 版權聲明

SwitchHosts 是一個免費開源軟體,基於 Apache-2.0 開源協議發佈。


================================================
FILE: alfred/Readme.txt
================================================
SwitchHosts! is an App for switching hosts quickly.

Homepage: https://oldj.github.io/SwitchHosts/

================================================
FILE: alfred/info.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>bundleid</key>
	<string>switchhosts.oldj.net</string>
	<key>category</key>
	<string>Tools</string>
	<key>connections</key>
	<dict>
		<key>E4D66445-FD72-47A2-9EE6-7232A2BADE29</key>
		<array>
			<dict>
				<key>destinationuid</key>
				<string>78D17FD5-9628-4901-A01A-511528D5FC14</string>
				<key>modifiers</key>
				<integer>0</integer>
				<key>modifiersubtext</key>
				<string></string>
				<key>vitoclose</key>
				<false/>
			</dict>
		</array>
	</dict>
	<key>createdby</key>
	<string>oldj</string>
	<key>description</key>
	<string>Switch hosts quickly!</string>
	<key>disabled</key>
	<false/>
	<key>name</key>
	<string>SwitchHosts</string>
	<key>objects</key>
	<array>
		<dict>
			<key>config</key>
			<dict>
				<key>concurrently</key>
				<false/>
				<key>escaping</key>
				<integer>102</integer>
				<key>script</key>
				<string>curl 'http://127.0.0.1:50761/api/toggle?id={query}'</string>
				<key>scriptargtype</key>
				<integer>0</integer>
				<key>scriptfile</key>
				<string></string>
				<key>type</key>
				<integer>0</integer>
			</dict>
			<key>type</key>
			<string>alfred.workflow.action.script</string>
			<key>uid</key>
			<string>78D17FD5-9628-4901-A01A-511528D5FC14</string>
			<key>version</key>
			<integer>2</integer>
		</dict>
		<dict>
			<key>config</key>
			<dict>
				<key>alfredfiltersresults</key>
				<false/>
				<key>alfredfiltersresultsmatchmode</key>
				<integer>0</integer>
				<key>argumenttreatemptyqueryasnil</key>
				<false/>
				<key>argumenttrimmode</key>
				<integer>0</integer>
				<key>argumenttype</key>
				<integer>1</integer>
				<key>escaping</key>
				<integer>68</integer>
				<key>keyword</key>
				<string>swh</string>
				<key>queuedelaycustom</key>
				<integer>3</integer>
				<key>queuedelayimmediatelyinitially</key>
				<true/>
				<key>queuedelaymode</key>
				<integer>0</integer>
				<key>queuemode</key>
				<integer>1</integer>
				<key>runningsubtext</key>
				<string>loading...</string>
				<key>script</key>
				<string>function makeItems(items) {
  return items.map(item =&gt; {
    return {
      uid: item.id,
      title: item.title,
      arg: item.id,
      icon: {path: item.on ? 'on.png' : 'off.png'},
      subtitle: (item.content || '').split('\n')[0],
    }
  })
}

function run(argv) {
  const server = 'http://127.0.0.1:50761'
  // console.log(argv)
  const queryURL = $.NSURL.URLWithString(`${server}/api/list`)
  const requestData = $.NSData.dataWithContentsOfURL(queryURL)
  const requestString = $.NSString.alloc.initWithDataEncoding(requestData, $.NSUTF8StringEncoding).js

  let result
  try {
    result = JSON.parse(requestString)
    result.data = result.data.filter((item)=&gt;item.title.includes("{query}"))
  } catch (e) {
    console.log(e)
    return JSON.stringify({
      items: [{
        uid: '0',
        title: `API Error: ${server}`,
        subtitle: 'Make sure SwitchHosts is running and the HTTP API interface is enabled.',
        valid: false,
      }]
    })
  }

  if (result.success) {
    return JSON.stringify({
      items: makeItems(result.data)
    })
  }

  return JSON.stringify({
    items: [{
      uid: '0',
      title: `Error: ${result.message || result.code || 'Unknown'}`,
      valid: false,
    }]
  })
}</string>
				<key>scriptargtype</key>
				<integer>0</integer>
				<key>scriptfile</key>
				<string></string>
				<key>subtext</key>
				<string>Switch hosts quickly!</string>
				<key>title</key>
				<string>Show hosts..</string>
				<key>type</key>
				<integer>7</integer>
				<key>withspace</key>
				<true/>
			</dict>
			<key>type</key>
			<string>alfred.workflow.input.scriptfilter</string>
			<key>uid</key>
			<string>E4D66445-FD72-47A2-9EE6-7232A2BADE29</string>
			<key>version</key>
			<integer>3</integer>
		</dict>
	</array>
	<key>readme</key>
	<string>This workflow is for the SwitchHosts App.

https://switchhosts.vercel.app</string>
	<key>uidata</key>
	<dict>
		<key>78D17FD5-9628-4901-A01A-511528D5FC14</key>
		<dict>
			<key>xpos</key>
			<integer>340</integer>
			<key>ypos</key>
			<integer>30</integer>
		</dict>
		<key>E4D66445-FD72-47A2-9EE6-7232A2BADE29</key>
		<dict>
			<key>xpos</key>
			<integer>120</integer>
			<key>ypos</key>
			<integer>30</integer>
		</dict>
	</dict>
	<key>variablesdontexport</key>
	<array/>
	<key>version</key>
	<string>1.3.0</string>
	<key>webaddress</key>
	<string>https://switchhosts.vercel.app</string>
</dict>
</plist>


================================================
FILE: app/package.json
================================================
{
  "name": "switchhosts",
  "productName": "SwitchHosts",
  "version": "4.3.0.6137",
  "description": "Switch hosts quickly!",
  "main": "./main.js",
  "author": {
    "name": "oldj",
    "email": "oldj.wu@gmail.com",
    "url": "https://github.com/oldj/SwitchHosts"
  },
  "homepage": "https://switchhosts.vercel.app",
  "scripts": {},
  "license": "Apache-2.0",
  "dependencies": {}
}

================================================
FILE: package.json
================================================
{
  "private": true,
  "scripts": {
    "start": "cross-env NODE_ENV=development electron ./build/main.js",
    "pretest": "rimraf ./test/tmp",
    "test": "vitest --config ./vitest.config.mts --watch=false",
    "typecheck": "tsc --noEmit",
    "clean:dist": "rimraf ./dist/*",
    "clean:build": "rimraf ./build/*",
    "dev": "npm run clean:build && concurrently --kill-others-on-fail --prefix-colors auto --names main,renderer \"npm run dev:main\" \"npm run dev:renderer\"",
    "dev:main": "vite build --watch --config ./vite.main.config.mts",
    "dev:renderer": "vite --config ./vite.render.config.mts",
    "version:up": "node scripts/version-up.mjs",
    "_build": "npm run version:up && npm run _build:release",
    "_build:release": "npm run clean:build && concurrently --kill-others-on-fail --prefix-colors auto --names main,renderer \"npm run build:main\" \"npm run build:renderer\"",
    "build:main": "cross-env NODE_ENV=production vite build --config ./vite.main.config.mts",
    "build:renderer": "cross-env NODE_ENV=production vite build --config ./vite.render.config.mts",
    "build": "npm run _build",
    "build:release": "npm run _build:release",
    "make": "node scripts/make.mjs",
    "make:dev": "cross-env SKIP_NOTARIZATION=1 cross-env MAKE_FOR=dev npm run make",
    "make:linux": "cross-env SKIP_NOTARIZATION=1 cross-env MAKE_FOR=linux npm run make",
    "make:win": "cross-env SKIP_NOTARIZATION=1 cross-env MAKE_FOR=win npm run make",
    "release:upload": "node ./scripts/upload-release.mjs",
    "release:upload:dry-run": "cross-env DRY_RUN=1 node ./scripts/upload-release.mjs",
    "publish": "npm run release:upload"
  },
  "dependencies": {
    "@hono/node-server": "^1.19.11",
    "axios": "1.13.6",
    "compare-versions": "6.1.1",
    "dayjs": "1.11.19",
    "electron-updater": "6.8.3",
    "electron-window-state": "5.0.3",
    "hono": "^4.12.7",
    "lodash": "4.17.23",
    "md5": "2.3.0",
    "md5-file": "5.0.0",
    "mkdirp": "3.0.1",
    "potdb": "2.6.6",
    "tslib": "2.8.1",
    "uuid": "13.0.0"
  },
  "devDependencies": {
    "@electron/notarize": "^3.1.1",
    "@mantine/core": "^8.3.16",
    "@mantine/hooks": "^8.3.16",
    "@tabler/icons-react": "3.38.0",
    "@types/assert": "1.5.11",
    "@types/lodash": "4.17.24",
    "@types/md5": "2.3.6",
    "@types/mkdirp": "2.0.0",
    "@types/node": "25.3.3",
    "@types/react": "19.2.14",
    "@types/react-dom": "19.2.3",
    "@types/semver": "7.7.1",
    "@types/uuid": "11.0.0",
    "@vitejs/plugin-react": "^5.1.4",
    "ahooks": "3.9.6",
    "chalk": "^5.6.2",
    "clsx": "2.1.1",
    "codejar": "^4.3.0",
    "codejar-linenumbers": "^1.0.1",
    "concurrently": "9.2.1",
    "cross-env": "10.1.0",
    "dotenv": "17.3.1",
    "electron": "39.5.1",
    "electron-builder": "26.8.1",
    "execa": "9.6.1",
    "fs-extra": "11.3.3",
    "jotai": "2.18.0",
    "prettier": "3.8.1",
    "pretty-bytes": "7.1.0",
    "progress": "^2.0.3",
    "react": "19.2.4",
    "react-dom": "19.2.4",
    "react-icons": "5.6.0",
    "react-router": "7.13.1",
    "rimraf": "^6.1.3",
    "sass": "1.97.3",
    "smooth-scroll-into-view-if-needed": "2.0.2",
    "typescript": "5.9.3",
    "vite": "7.3.1",
    "vite-plugin-static-copy": "3.2.0",
    "vite-tsconfig-paths": "6.1.1",
    "vitest": "^3.2.4"
  }
}


================================================
FILE: scripts/entitlements.mac.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
        <true/>
        <key>com.apple.security.cs.allow-jit</key>
        <true/>
    </dict>
</plist>


================================================
FILE: scripts/hooks/artifactBuildCompleted.mjs
================================================
import { notarize } from '@electron/notarize'
import path from 'node:path'
import { isEnvFlagEnabled } from '../libs/build-env.mjs'
import { getNotarizeOptions } from './notarize-options.mjs'

export default async function artifactBuildCompleted(context) {
  const { file, packager } = context

  if (!file || path.extname(file) !== '.dmg') {
    return
  }

  if (packager?.platform?.name !== 'mac') {
    return
  }

  if (process.env.MAKE_FOR === 'dev' || isEnvFlagEnabled(process.env.SKIP_NOTARIZATION)) {
    console.log(`skip notarization for ${path.basename(file)}.`)
    return
  }

  const options = await getNotarizeOptions(file)
  if (!options) {
    throw new Error(`Notarization credentials are missing for ${path.basename(file)}.`)
  }

  console.log('in artifactBuildCompleted, notarize dmg...')
  console.log(`dmgPath: ${file}`)
  await notarize(options)
  console.log(`Notarize done for ${path.basename(file)}.`)
}


================================================
FILE: scripts/hooks/notarize-options.mjs
================================================
import { execFile } from 'node:child_process'
import { isEnvFlagEnabled } from '../libs/build-env.mjs'

function getPasswordFromKeychain(account, service) {
  return new Promise((resolve, reject) => {
    execFile(
      'security',
      ['find-generic-password', '-a', account, '-s', service, '-w'],
      (error, stdout, stderr) => {
        if (error) {
          reject(new Error(stderr || error.message))
          return
        }

        resolve(stdout.trim())
      },
    )
  })
}

export async function prepareNotarizeEnv(env = process.env) {
  if (isEnvFlagEnabled(env.SKIP_NOTARIZATION) || env.MAKE_FOR === 'dev') {
    return env
  }

  if (!env.APPLE_TEAM_ID && env.TEAM_ID) {
    env.APPLE_TEAM_ID = env.TEAM_ID
  }

  const hasCompleteCredentials =
    !!env.APPLE_KEYCHAIN_PROFILE ||
    (!!env.APPLE_API_KEY && !!env.APPLE_API_KEY_ID && !!env.APPLE_API_ISSUER) ||
    (!!env.APPLE_ID && !!env.APPLE_APP_SPECIFIC_PASSWORD && !!env.APPLE_TEAM_ID)

  if (hasCompleteCredentials || !env.APPLE_ID) {
    return env
  }

  try {
    env.APPLE_APP_SPECIFIC_PASSWORD = await getPasswordFromKeychain(
      env.APPLE_ID,
      `Apple Notarize: ${env.APPLE_ID}`,
    )
  } catch (error) {
    console.log(`Legacy notarization keychain lookup skipped: ${error.message}`)
  }

  return env
}

export function hasNotarizeCredentials(env = process.env) {
  return Boolean(
    env.APPLE_KEYCHAIN_PROFILE ||
      (env.APPLE_API_KEY && env.APPLE_API_KEY_ID && env.APPLE_API_ISSUER) ||
      (env.APPLE_ID && env.APPLE_APP_SPECIFIC_PASSWORD && env.APPLE_TEAM_ID),
  )
}

export async function getNotarizeOptions(appPath, env = process.env) {
  await prepareNotarizeEnv(env)

  const {
    APPLE_API_KEY: appleApiKey,
    APPLE_API_KEY_ID: appleApiKeyId,
    APPLE_API_ISSUER: appleApiIssuer,
    APPLE_ID: appleId,
    APPLE_APP_SPECIFIC_PASSWORD: appleIdPassword,
    APPLE_KEYCHAIN: keychain,
    APPLE_KEYCHAIN_PROFILE: keychainProfile,
    APPLE_TEAM_ID: teamId,
  } = env

  const tool = 'notarytool'

  if (appleId || appleIdPassword) {
    if (!appleId) {
      throw new Error('APPLE_ID env var needs to be set')
    }
    if (!appleIdPassword) {
      throw new Error('APPLE_APP_SPECIFIC_PASSWORD env var needs to be set')
    }
    if (!teamId) {
      throw new Error('APPLE_TEAM_ID env var needs to be set')
    }
    return { tool, appPath, appleId, appleIdPassword, teamId }
  }

  if (appleApiKey || appleApiKeyId || appleApiIssuer) {
    if (!appleApiKey || !appleApiKeyId || !appleApiIssuer) {
      throw new Error('Env vars APPLE_API_KEY, APPLE_API_KEY_ID and APPLE_API_ISSUER need to be set')
    }
    return { tool, appPath, appleApiKey, appleApiKeyId, appleApiIssuer }
  }

  if (keychainProfile) {
    return {
      tool,
      appPath,
      keychainProfile,
      ...(keychain ? { keychain } : {}),
    }
  }

  return null
}


================================================
FILE: scripts/libs/build-env.mjs
================================================
export function hasValue(value) {
  return typeof value === 'string' ? value.trim() !== '' : Boolean(value)
}

export function getFirstConfiguredEnv(env, names) {
  for (const name of names) {
    if (hasValue(env[name])) {
      return env[name].trim()
    }
  }

  return null
}

export function isEnvFlagEnabled(value) {
  if (!hasValue(value)) {
    return false
  }

  if (typeof value === 'string') {
    const normalized = value.trim().toLowerCase()
    if (['0', 'false', 'no', 'off', ''].includes(normalized)) {
      return false
    }

    if (['1', 'true', 'yes', 'on'].includes(normalized)) {
      return true
    }
  }

  return Boolean(value)
}


================================================
FILE: scripts/libs/build-log.mjs
================================================
import chalk from 'chalk'
import dayjs from 'dayjs'

export const PLATFORM_LABELS = {
  mac: 'macOS',
  win: 'Windows',
  linux: 'Linux',
}

const PLATFORM_COLORS = {
  mac: chalk.magenta,
  win: chalk.cyan,
  linux: chalk.green,
}

export function formatTimestamp(date = new Date()) {
  return dayjs(date).format('YYYY-MM-DD HH:mm:ss')
}

function formatLogLine(message) {
  return `${formatTimestamp()} ${message}`
}

export function formatDuration(ms) {
  const totalSeconds = Math.floor(ms / 1000)
  const hours = Math.floor(totalSeconds / 3600)
  const minutes = Math.floor((totalSeconds % 3600) / 60)
  const seconds = totalSeconds % 60

  if (hours > 0) {
    return `${hours}h ${minutes}m ${seconds}s`
  }

  if (minutes > 0) {
    return `${minutes}m ${seconds}s`
  }

  return `${seconds}s`
}

export function logBanner(message) {
  console.log(chalk.bold.blue(`\n${formatLogLine(`=== ${message} ===`)}`))
}

export function logStep(message) {
  console.log(chalk.blue(formatLogLine(`-> ${message}`)))
}

export function logSuccess(message) {
  console.log(chalk.green(formatLogLine(`✓ ${message}`)))
}

export function logWarning(message) {
  console.log(chalk.yellow(formatLogLine(`! ${message}`)))
}

export function logPlatform(platform, message) {
  const color = PLATFORM_COLORS[platform] || chalk.white
  const label = PLATFORM_LABELS[platform] || platform
  console.log(color(formatLogLine(`[${label}] ${message}`)))
}


================================================
FILE: scripts/libs/build-plan.mjs
================================================
import { Arch } from 'builder-util'
import path from 'node:path'
import { PLATFORM_LABELS, formatDuration, logBanner, logPlatform } from './build-log.mjs'

export const PLATFORM_ORDER = ['mac', 'win', 'linux']

function resolvePlatformName(name) {
  const map = {
    darwin: 'mac',
    linux: 'linux',
    mac: 'mac',
    win: 'win',
    win32: 'win',
    windows: 'win',
  }

  return map[name] || null
}

function formatArch(arch) {
  if (arch == null) {
    return 'unknown'
  }

  return Arch[arch] || String(arch)
}

export function getBuildPlan(makeFor, targetPlatformsConfigs) {
  if (makeFor === 'dev') {
    return [{ platform: 'mac', targets: targetPlatformsConfigs.mac.mac }]
  }

  if (makeFor === 'mac') {
    return [{ platform: 'mac', targets: targetPlatformsConfigs.mac.mac }]
  }

  if (makeFor === 'win') {
    return [{ platform: 'win', targets: targetPlatformsConfigs.win.win }]
  }

  if (makeFor === 'linux') {
    return [{ platform: 'linux', targets: targetPlatformsConfigs.linux.linux }]
  }

  return PLATFORM_ORDER.map((platform) => ({
    platform,
    targets: targetPlatformsConfigs.all[platform],
  }))
}

export function createBuildTracker({ plan, compression, macBuildState, winBuildState, artifactBuildCompletedHook }) {
  // Track platform timing through electron-builder hooks while the outer loop
  // runs one platform build at a time for cleaner, non-interleaved logging.
  const stats = new Map(
    plan.map(({ platform, targets }) => [
      platform,
      {
        targets,
        startedAt: 0,
        finishedAt: 0,
      },
    ]),
  )

  function getStat(platform) {
    if (!stats.has(platform)) {
      stats.set(platform, {
        targets: [],
        startedAt: 0,
        finishedAt: 0,
      })
    }

    return stats.get(platform)
  }

  function markStarted(platform) {
    const stat = getStat(platform)

    if (!stat.startedAt) {
      stat.startedAt = Date.now()
      logBanner(`Build ${PLATFORM_LABELS[platform]}`)
      logPlatform(platform, `targets: ${stat.targets.join(', ')}`)
      logPlatform(platform, `compression: ${compression}`)
      if (platform === 'mac') {
        logPlatform(platform, `code signing: ${macBuildState.sign ? 'enabled' : 'disabled'}`)
        logPlatform(platform, `notarization: ${macBuildState.notarize ? 'enabled' : 'disabled'}`)
      } else if (platform === 'win') {
        logPlatform(platform, `code signing: ${winBuildState.sign ? 'enabled' : 'disabled'}`)
      } else {
        logPlatform(platform, 'notarization: disabled')
      }
    }

    return stat
  }

  function markFinished(platform) {
    const stat = getStat(platform)
    stat.finishedAt = Date.now()
    return stat
  }

  return {
    hooks: {
      beforePack(context) {
        const platform = resolvePlatformName(context.electronPlatformName)
        if (!platform) {
          return
        }

        markStarted(platform)
        // beforePack fires for each arch-specific app bundle preparation.
        logPlatform(platform, `packaging app bundle for ${formatArch(context.arch)}...`)
      },

      afterPack(context) {
        const platform = resolvePlatformName(context.electronPlatformName)
        if (!platform) {
          return
        }

        markFinished(platform)
        logPlatform(platform, `app bundle ready for ${formatArch(context.arch)}`)
      },

      async artifactBuildCompleted(context) {
        const platform = resolvePlatformName(context.packager?.platform?.name)
        if (platform) {
          markStarted(platform)
        }

        // Reuse the DMG notarization hook from the packaging config so logging and
        // timing stay in one place while the notarization logic remains isolated.
        const artifactFile = context.file || ''
        const isMacDmg = platform === 'mac' && path.extname(artifactFile) === '.dmg'
        if (isMacDmg && !macBuildState.notarize) {
          logPlatform(platform, `skipping dmg notarization: ${path.basename(artifactFile)}`)
        } else {
          await artifactBuildCompletedHook(context)
        }

        if (!platform) {
          return
        }

        markFinished(platform)
        const targetName = context.target?.name || path.extname(artifactFile).slice(1)
        logPlatform(platform, `artifact ready (${targetName}): ${path.basename(artifactFile)}`)
      },
    },

    printSummary() {
      logBanner('Build Summary')
      for (const { platform } of plan) {
        const stat = getStat(platform)
        const elapsed = stat.startedAt && stat.finishedAt ? stat.finishedAt - stat.startedAt : 0
        logPlatform(platform, `elapsed: ${elapsed > 0 ? formatDuration(elapsed) : 'n/a'}`)
      }
    },
  }
}


================================================
FILE: scripts/libs/build-state.mjs
================================================
import { hasNotarizeCredentials, prepareNotarizeEnv } from '../hooks/notarize-options.mjs'
import { getFirstConfiguredEnv, hasValue, isEnvFlagEnabled } from './build-env.mjs'

function hasSigningIdentityEnv(env = process.env) {
  return hasValue(env.IDENTITY)
}

function describeNotarizationSetup(env = process.env) {
  if (hasValue(env.APPLE_KEYCHAIN_PROFILE)) {
    return 'APPLE_KEYCHAIN_PROFILE'
  }

  if (hasValue(env.APPLE_API_KEY) || hasValue(env.APPLE_API_KEY_ID) || hasValue(env.APPLE_API_ISSUER)) {
    return 'APPLE_API_KEY + APPLE_API_KEY_ID + APPLE_API_ISSUER'
  }

  if (
    hasValue(env.APPLE_ID) ||
    hasValue(env.APPLE_APP_SPECIFIC_PASSWORD) ||
    hasValue(env.APPLE_TEAM_ID) ||
    hasValue(env.TEAM_ID)
  ) {
    return 'APPLE_ID + APPLE_APP_SPECIFIC_PASSWORD + APPLE_TEAM_ID'
  }

  return null
}

export async function resolveMacBuildState(plan, env = process.env) {
  const includesMac = plan.some(({ platform }) => platform === 'mac')
  const notarizationForcedOff = env.MAKE_FOR === 'dev' || isEnvFlagEnabled(env.SKIP_NOTARIZATION)

  const state = {
    includesMac,
    sign: false,
    notarize: false,
    logLevel: 'step',
    message: 'macOS signing configuration check skipped',
  }

  if (!includesMac) {
    state.message = 'skipping macOS signing configuration check'
    return state
  }

  await prepareNotarizeEnv(env)

  const hasIdentity = hasSigningIdentityEnv(env)
  const hasNotary = hasNotarizeCredentials(env)
  const configuredNotarySetup = describeNotarizationSetup(env)

  if (notarizationForcedOff) {
    if (hasIdentity) {
      state.sign = true
      state.logLevel = 'success'
      state.message = `macOS code signing enabled via IDENTITY; notarization disabled by ${
        env.MAKE_FOR === 'dev' ? 'MAKE_FOR=dev' : 'SKIP_NOTARIZATION'
      }`
    } else {
      state.logLevel = 'warning'
      state.message =
        'IDENTITY is not configured; falling back to unsigned macOS artifacts because notarization is disabled.'
    }

    return state
  }

  if (hasIdentity && hasNotary) {
    state.sign = true
    state.notarize = true
    state.logLevel = 'success'
    state.message = `macOS signing and notarization enabled via IDENTITY + ${configuredNotarySetup}`
    return state
  }

  const missing = []
  if (!hasIdentity) {
    missing.push('IDENTITY')
  }
  if (!hasNotary) {
    missing.push(
      'APPLE_KEYCHAIN_PROFILE or APPLE_API_KEY/APPLE_API_KEY_ID/APPLE_API_ISSUER or APPLE_ID/APPLE_APP_SPECIFIC_PASSWORD/APPLE_TEAM_ID',
    )
  }

  state.logLevel = 'warning'
  state.message =
    `macOS signing/notarization config is missing or incomplete (${missing.join(', ')}). ` +
    'Falling back to unsigned and unnotarized macOS artifacts.'

  return state
}

export function resolveWindowsBuildState(plan, env = process.env) {
  const includesWin = plan.some(({ platform }) => platform === 'win')
  const certificateSubjectName = getFirstConfiguredEnv(env, [
    'WIN_CERTIFICATE_SUBJECT_NAME',
    'WINDOWS_CERTIFICATE_SUBJECT_NAME',
    'WIN_CERT_SUBJECT_NAME',
  ])
  const configuredPublisherName = getFirstConfiguredEnv(env, ['WIN_PUBLISHER_NAME', 'WINDOWS_PUBLISHER_NAME'])
  const publisherName = configuredPublisherName || certificateSubjectName

  const state = {
    includesWin,
    sign: false,
    logLevel: 'step',
    message: 'skipping Windows signing configuration check',
    publisherName,
    certificateSubjectName,
  }

  if (!includesWin) {
    state.message = 'skipping Windows signing configuration check'
    return state
  }

  if (certificateSubjectName) {
    state.sign = true
    state.logLevel = 'success'
    state.message =
      configuredPublisherName
        ? 'Windows code signing enabled via WIN_CERTIFICATE_SUBJECT_NAME and WIN_PUBLISHER_NAME.'
        : 'Windows code signing enabled via WIN_CERTIFICATE_SUBJECT_NAME; publisherName defaults to the certificate subject name.'
    return state
  }

  if (configuredPublisherName) {
    state.logLevel = 'warning'
    state.message =
      'Windows signing config is incomplete (missing WIN_CERTIFICATE_SUBJECT_NAME or WINDOWS_CERTIFICATE_SUBJECT_NAME or WIN_CERT_SUBJECT_NAME). ' +
      'Skipping Windows code signing for this build.'
    return state
  }

  state.message =
    'Windows code signing disabled by default. Set WIN_CERTIFICATE_SUBJECT_NAME to enable it; WIN_PUBLISHER_NAME is optional.'
  return state
}


================================================
FILE: scripts/libs/my-exec.mjs
================================================
import { spawn } from 'node:child_process'

export default function myExec(cmd, ...args) {
  return new Promise((resolve, reject) => {
    const run = spawn(cmd, args)

    let out = ''

    run.stdout.on('data', (data) => {
      console.log(`[stdout]: ${data.toString().trimEnd()}`)
      out += data.toString()
    })

    run.stderr.on('data', (data) => {
      console.log(`[stderr]: ${data.toString().trimEnd()}`)
    })

    run.on('exit', function (code) {
      console.log('child process exited with code ' + code.toString())
      if (code === 0) {
        resolve(out)
      } else {
        reject(code)
      }
    })
  })
}


================================================
FILE: scripts/make.mjs
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import chalk from 'chalk'
import { config as loadEnv } from 'dotenv'
import fse from 'fs-extra'
import { createRequire } from 'node:module'
import { homedir } from 'node:os'
import path from 'node:path'
import artifactBuildCompletedHook from './hooks/artifactBuildCompleted.mjs'
import { PLATFORM_LABELS, formatDuration, logBanner, logPlatform, logStep, logSuccess, logWarning } from './libs/build-log.mjs'
import { createBuildTracker, getBuildPlan } from './libs/build-plan.mjs'
import { resolveMacBuildState, resolveWindowsBuildState } from './libs/build-state.mjs'
import { resolveGithubRepository } from './release-config.mjs'
import { APP_NAME, distDir, electronLanguages, rootDir } from './vars.mjs'

loadEnv()

// Use CommonJS require for local JSON/package reads so the script stays portable
// across Node runtimes without relying on JSON import assertions.
const require = createRequire(import.meta.url)
const version = require('../src/version.json')

const TARGET_PLATFORMS_CONFIGS = {
  mac: {
    mac: ['dmg:x64', 'dmg:arm64'],
  },
  win: {
    win: ['nsis:ia32', 'nsis:x64', 'nsis:arm64', 'portable:x64'],
  },
  linux: {
    linux: ['AppImage:x64', 'AppImage:arm64', 'deb:x64', 'deb:arm64'],
  },
  all: {
    mac: ['dmg:x64', 'dmg:arm64', 'zip:universal'],
    win: ['nsis:ia32', 'nsis:x64', 'nsis:arm64', 'portable:x64', 'zip:x64' /* , 'appx:x64'*/],
    linux: ['AppImage:x64', 'AppImage:arm64', 'deb:x64', 'deb:arm64'],
  },
}

const { APP_BUNDLE_ID, IDENTITY, MAKE_FOR } = process.env
const appId = APP_BUNDLE_ID || 'SwitchHosts'
const fullVersion = `${version[0]}.${version[1]}.${version[2]}.${version[3]}`
const publishMode = process.env.PUBLISH_POLICY || 'never'
const githubRepository = resolveGithubRepository(process.env)
const WINDOWS_TIMESTAMP_SERVER = 'http://rfc3161timestamp.globalsign.com/advanced'

function createBuilderConfig(hooks, macBuildState, winBuildState) {
  // Build the full electron-builder config in one place so every entrypoint
  // (`make`, `make:*`) stays on the same packaging pipeline.
  return {
    ...cfgCommon,
    appId,
    productName: APP_NAME,
    mac: {
      type: 'distribution',
      category: 'public.app-category.productivity',
      icon: 'assets/app.icns',
      gatekeeperAssess: false,
      electronLanguages,
      identity: macBuildState.sign ? IDENTITY : null,
      hardenedRuntime: true,
      entitlements: 'scripts/entitlements.mac.plist',
      entitlementsInherit: 'scripts/entitlements.mac.plist',
      extendInfo: {
        ITSAppUsesNonExemptEncryption: false,
        CFBundleLocalizations: electronLanguages,
        CFBundleDevelopmentRegion: 'en',
      },
      artifactName: '${productName}-v' + fullVersion + '-${arch}-mac.${ext}',
      ...(macBuildState.notarize ? {} : { notarize: false }),
    },
    dmg: {
      background: 'assets/dmg-bg.png',
      iconSize: 160,
      window: {
        width: 600,
        height: 420,
      },
      contents: [
        {
          x: 150,
          y: 200,
        },
        {
          x: 450,
          y: 200,
          type: 'link',
          path: '/Applications',
        },
      ],
      sign: macBuildState.sign,
      artifactName: '${productName}-v' + fullVersion + '-mac-${arch}.${ext}',
    },
    win: {
      icon: 'assets/icon.ico',
      verifyUpdateCodeSignature: winBuildState.sign,
      signAndEditExecutable: winBuildState.sign,
      // NSIS/portable targets still try to sign final `.exe` artifacts unless
      // we explicitly exclude them when Windows signing is disabled.
      ...(winBuildState.sign ? {} : { signExts: ['!.exe'] }),
      ...(winBuildState.sign
        ? {
            signtoolOptions: {
              signingHashAlgorithms: ['sha256'],
              publisherName: winBuildState.publisherName,
              certificateSubjectName: winBuildState.certificateSubjectName,
              timeStampServer: WINDOWS_TIMESTAMP_SERVER,
              rfc3161TimeStampServer: WINDOWS_TIMESTAMP_SERVER,
            },
          }
        : {}),
      artifactName: '${productName}-v' + fullVersion + '-win-${arch}.${ext}',
    },
    nsis: {
      installerIcon: 'assets/installer-icon.ico',
      oneClick: false,
      allowToChangeInstallationDirectory: true,
      deleteAppDataOnUninstall: false,
      shortcutName: 'SwitchHosts',
      artifactName: '${productName}-v' + fullVersion + '-win-${arch}-installer.${ext}',
    },
    portable: {
      artifactName: '${productName}-v' + fullVersion + '-win-${arch}-portable.${ext}',
    },
    linux: {
      icon: 'assets/app.icns',
      artifactName: '${productName}-v' + fullVersion + '-linux-${arch}.${ext}',
      category: 'Utility',
      synopsis: 'An App for hosts management and switching.',
      desktop: {
        entry: {
          Name: 'SwitchHosts',
          Type: 'Application',
          GenericName: 'An App for hosts management and switching.',
        },
      },
    },
    publish: {
      // Keep the GitHub provider configured so electron-builder emits update metadata
      // for GitHub Releases, while the actual asset upload stays in scripts/upload-release.mjs.
      provider: 'github',
      owner: githubRepository.owner,
      repo: githubRepository.repo,
      releaseType: 'draft',
      vPrefixedTagName: true,
    },
    beforePack: hooks.beforePack,
    afterPack: hooks.afterPack,
    artifactBuildCompleted: hooks.artifactBuildCompleted,
  }
}

if (!APP_BUNDLE_ID) {
  logWarning('APP_BUNDLE_ID is not set, falling back to appId "SwitchHosts".')
}
logStep(`APP_BUNDLE_ID: ${APP_BUNDLE_ID || '(fallback: SwitchHosts)'}`)

const cfgCommon = {
  copyright: `Copyright © ${new Date().getFullYear()}`,
  buildVersion: version[3].toString(),
  directories: {
    buildResources: 'build',
    app: 'build',
    output: 'dist',
  },
  electronDownload: {
    cache: path.join(homedir(), '.electron'),
    mirror: 'https://registry.npmmirror.com/-/binary/electron/',
  },
  asar: true,
  compression: 'maximum',
}

const beforeMake = async () => {
  const t0 = Date.now()
  logBanner('Prepare Build Directory')

  // Start every package run from a clean dist directory to avoid mixing artifacts
  // from different target sets or previous versions.
  fse.removeSync(distDir)
  fse.ensureDirSync(distDir)
  logStep(`dist cleaned: ${distDir}`)

  const toCopy = [[path.join(rootDir, 'assets', 'app.png'), path.join(rootDir, 'build', 'assets', 'app.png')]]

  toCopy.map(([src, target]) => {
    fse.copySync(src, target)
  })
  logStep(`copied build assets: ${toCopy.map(([src]) => path.basename(src)).join(', ')}`)

  let pkgBase = require(path.join(rootDir, 'package.json'))
  let pkgApp = require(path.join(rootDir, 'app', 'package.json'))

  // Refresh the app package manifest inside build/ so electron-builder always
  // packages the current dependency set and release version.
  pkgApp.name = APP_NAME
  pkgApp.version = version.slice(0, 3).join('.')
  pkgApp.dependencies = pkgBase.dependencies

  fse.writeFileSync(
    path.join(rootDir, 'build', 'package.json'),
    JSON.stringify(pkgApp, null, 2),
    'utf-8',
  )
  logSuccess(`build/package.json refreshed in ${formatDuration(Date.now() - t0)}`)
}

const afterMake = async () => {
  const t0 = Date.now()
  logBanner('Finalize Packaging')

  // Reserved for post-build cleanup or metadata fixes if packaging needs them later.
  logSuccess(`post-build steps finished in ${formatDuration(Date.now() - t0)}`)
}

const doMake = async () => {
  // Resolve the requested platform set first so every later step can log against
  // the same plan and timing model.
  const compression = MAKE_FOR === 'dev' ? 'store' : 'maximum'
  cfgCommon.compression = compression
  const plan = getBuildPlan(MAKE_FOR, TARGET_PLATFORMS_CONFIGS)
  const macBuildState = await resolveMacBuildState(plan)
  const winBuildState = resolveWindowsBuildState(plan)
  const tracker = createBuildTracker({
    plan,
    compression,
    macBuildState,
    winBuildState,
    artifactBuildCompletedHook,
  })

  logBanner('Build Plan')
  logStep(`MAKE_FOR: ${MAKE_FOR || 'all'}`)
  logStep(`version: ${fullVersion}`)
  logStep(`appId: ${appId}`)
  logStep(`compression: ${cfgCommon.compression}`)
  logStep(`publish: ${publishMode}`)
  logStep(`platforms: ${plan.map(({ platform }) => PLATFORM_LABELS[platform]).join(', ')}`)
  if (macBuildState.includesMac) {
    if (macBuildState.logLevel === 'warning') {
      logWarning(macBuildState.message)
    } else if (macBuildState.logLevel === 'success') {
      logSuccess(macBuildState.message)
    } else {
      logStep(macBuildState.message)
    }
  }
  if (winBuildState.includesWin) {
    if (winBuildState.logLevel === 'warning') {
      logWarning(winBuildState.message)
    } else if (winBuildState.logLevel === 'success') {
      logSuccess(winBuildState.message)
    } else {
      logStep(winBuildState.message)
    }
  }

  if (macBuildState.notarize) {
    logStep('notarization environment prepared')
  } else if (macBuildState.includesMac) {
    logStep('running macOS packaging without notarization')
  } else {
    logStep('skipping macOS notarization preparation')
  }

  logStep('loading electron-builder...')
  const eb = await import('electron-builder')
  const builder = eb.default || eb
  logSuccess('electron-builder loaded')

  // Build one platform per invocation so electron-builder's own logs stay grouped
  // and easy to read even when each platform expands to multiple arch/target jobs.
  for (const { platform, targets } of plan) {
    logPlatform(platform, 'starting electron-builder run...')
    await builder.build({
      [platform]: targets,
      publish: publishMode,
      config: createBuilderConfig(tracker.hooks, macBuildState, winBuildState),
    })
    logPlatform(platform, 'electron-builder run finished.')
  }

  tracker.printSummary()
}

async function main() {
  const t0 = Date.now()
  try {
    // The top-level flow is intentionally linear: prepare inputs, run packaging,
    // then finish with summary output and any future cleanup.
    await beforeMake()
    await doMake()
    await afterMake()

    logBanner('Done')
    logSuccess(`total elapsed: ${formatDuration(Date.now() - t0)}`)
  } catch (e) {
    logBanner('Build Failed')
    console.error(chalk.red(e?.stack || String(e)))
    console.log(chalk.red(`total elapsed before failure: ${formatDuration(Date.now() - t0)}`))
    process.exit(1)
  }
}

await main()


================================================
FILE: scripts/release-config.mjs
================================================
import { createRequire } from 'node:module'

const require = createRequire(import.meta.url)
const version = require('../src/version.json')

export const DEFAULT_GITHUB_REPOSITORY = 'oldj/SwitchHosts'

export function getReleaseVersion() {
  return version.slice(0, 3).join('.')
}

export function getFullVersion() {
  return `${version[0]}.${version[1]}.${version[2]}.${version[3]}`
}

export function getReleaseTag(env = process.env) {
  const expectedTag = `v${getReleaseVersion()}`
  const tag = env.RELEASE_TAG || expectedTag

  // Keep GitHub Release tags aligned with the app's public semver so
  // the uploader cannot silently publish assets under a mismatched tag.
  if (tag !== expectedTag) {
    throw new Error(`RELEASE_TAG must be "${expectedTag}", got "${tag}".`)
  }

  return tag
}

export function resolveGithubRepository(env = process.env) {
  const rawRepository =
    env.GH_RELEASE_REPOSITORY || env.GITHUB_REPOSITORY || DEFAULT_GITHUB_REPOSITORY

  const match = /^([^/\s]+)\/([^/\s]+)$/.exec(rawRepository || '')
  if (!match) {
    throw new Error(
      `Invalid GitHub repository "${rawRepository}". Expected the format "owner/repo".`,
    )
  }

  const [, owner, repo] = match

  return {
    owner,
    repo,
    fullName: `${owner}/${repo}`,
  }
}

export function isReleaseArtifactFile(fileName, fullVersion = getFullVersion()) {
  if (!fileName || fileName.startsWith('.')) {
    return false
  }

  // builder-debug.yml is useful locally, but publishing it would only clutter the release page.
  if (fileName === 'builder-debug.yml') {
    return false
  }

  // latest*.yml files are required by electron-updater to discover GitHub-hosted updates.
  if (/^latest.*\.ya?ml$/i.test(fileName)) {
    return true
  }

  return fileName.includes(`v${fullVersion}`)
}


================================================
FILE: scripts/upload-diagnostics.mjs
================================================
function getCauseField(cause, field) {
  if (!cause || !(field in cause)) return null
  const value = cause[field]
  return value === null || value === undefined ? null : String(value)
}

function normalizeTarget(target) {
  if (!target) {
    return null
  }

  if (typeof target === 'string') {
    return target
  }

  if (typeof target === 'object' && target !== null) {
    if ('pathname' in target && typeof target.pathname === 'string') {
      const search = 'search' in target && typeof target.search === 'string' ? target.search : ''
      return `${target.pathname}${search}`
    }

    if ('href' in target && typeof target.href === 'string') {
      return target.href
    }
  }

  return String(target)
}

function getCause(error) {
  if (!(error instanceof Error)) {
    return null
  }

  if (typeof error.cause === 'object' && error.cause !== null) {
    return error.cause
  }

  return null
}

function pickEnumerableFields(value) {
  if (!value || typeof value !== 'object') {
    return null
  }

  const entries = Object.entries(value)
    .filter(([, entryValue]) => {
      return entryValue === null || [ 'string', 'number', 'boolean' ].includes(typeof entryValue)
    })

  return entries.length > 0 ? Object.fromEntries(entries) : null
}

export function extractErrorDetails(error) {
  const normalizedError = error instanceof Error ? error : new Error(String(error))
  const cause = getCause(normalizedError)

  return {
    causeCode: getCauseField(cause, 'code'),
    causeErrno: getCauseField(cause, 'errno'),
    causeHostname: getCauseField(cause, 'hostname'),
    causeMessage: getCauseField(cause, 'message'),
    causeSyscall: getCauseField(cause, 'syscall'),
    errorMessage: normalizedError.message,
    errorName: normalizedError.name || 'Error',
    rawCause: pickEnumerableFields(cause),
    stack: normalizedError.stack || null,
  }
}

export function buildDiagnostic({
  attempt,
  error,
  fileIndex = null,
  fileName = null,
  httpStatus = null,
  maxAttempts,
  method,
  progressSnapshot = null,
  retryable,
  stage,
  target = null,
}) {
  const errorDetails = extractErrorDetails(error)

  return {
    attempt,
    causeCode: errorDetails.causeCode,
    causeErrno: errorDetails.causeErrno,
    causeHostname: errorDetails.causeHostname,
    causeMessage: errorDetails.causeMessage,
    causeSyscall: errorDetails.causeSyscall,
    currentFileBytes: progressSnapshot?.currentFileBytes ?? null,
    errorMessage: errorDetails.errorMessage,
    errorName: errorDetails.errorName,
    fileIndex,
    fileName,
    httpStatus,
    maxAttempts,
    method,
    retryable: Boolean(retryable),
    stage,
    target: normalizeTarget(target),
    totalFiles: progressSnapshot?.totalFiles ?? null,
    totalUploadedBytes: progressSnapshot?.totalUploadedBytes ?? null,
  }
}

export function formatDiagnosticSummary(diagnostic) {
  const subject = diagnostic.fileName || diagnostic.target || diagnostic.stage
  const details = [ `attempt ${diagnostic.attempt}/${diagnostic.maxAttempts}` ]

  if (diagnostic.fileIndex != null && diagnostic.totalFiles) {
    details.push(`file ${diagnostic.fileIndex}/${diagnostic.totalFiles}`)
  }

  if (diagnostic.httpStatus) {
    details.push(`status=${diagnostic.httpStatus}`)
  }

  if (diagnostic.causeCode) {
    details.push(`cause=${diagnostic.causeCode}`)
  }

  if (diagnostic.causeMessage) {
    details.push(`message=${diagnostic.causeMessage}`)
  } else if (diagnostic.errorMessage) {
    details.push(`message=${diagnostic.errorMessage}`)
  }

  return `${diagnostic.stage} failed for ${subject} (${details.join(', ')})`
}

export function formatRetrySummary(diagnostic, delayLabel) {
  const subject = diagnostic.fileName || diagnostic.target || diagnostic.stage
  const details = [ `attempt ${Math.min(diagnostic.attempt + 1, diagnostic.maxAttempts)}/${diagnostic.maxAttempts}` ]

  if (diagnostic.fileIndex != null && diagnostic.totalFiles) {
    details.unshift(`file ${diagnostic.fileIndex}/${diagnostic.totalFiles}`)
  }

  if (diagnostic.httpStatus) {
    details.push(`status=${diagnostic.httpStatus}`)
  }

  if (diagnostic.causeCode) {
    details.push(`cause=${diagnostic.causeCode}`)
  }

  details.push(`in ${delayLabel}`)

  return `retrying ${diagnostic.stage} ${subject} (${details.join(', ')})`
}

export function buildDebugPayload(diagnostic, error) {
  const errorDetails = extractErrorDetails(error)

  return {
    diagnostic,
    error: {
      cause: errorDetails.rawCause,
      errorMessage: errorDetails.errorMessage,
      errorName: errorDetails.errorName,
      stack: errorDetails.stack,
    },
  }
}

export function attachDiagnostic(error, diagnostic) {
  const normalizedError = error instanceof Error ? error : new Error(String(error))
  normalizedError.diagnostic = diagnostic
  return normalizedError
}


================================================
FILE: scripts/upload-progress.mjs
================================================
import prettyBytes from 'pretty-bytes'
import ProgressBar from 'progress'

const PROGRESS_BAR_FORMAT = '[:bar]'
const PROGRESS_BAR_WIDTH = 24

function clamp(value, min, max) {
  return Math.min(Math.max(value, min), max)
}

export function formatPercent(value) {
  return `${clamp(value, 0, 100).toFixed(1)}%`
}

export function formatEta(seconds) {
  if (!Number.isFinite(seconds) || seconds < 0) {
    return '--:--'
  }

  const roundedSeconds = Math.ceil(seconds)
  const hours = Math.floor(roundedSeconds / 3600)
  const minutes = Math.floor((roundedSeconds % 3600) / 60)
  const secs = roundedSeconds % 60

  if (hours > 0) {
    return [ hours, minutes, secs ].map((value) => String(value).padStart(2, '0')).join(':')
  }

  return `${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`
}

export function truncateFileName(fileName, maxLength = 36) {
  if (fileName.length <= maxLength) {
    return fileName
  }

  if (maxLength <= 3) {
    return fileName.slice(0, maxLength)
  }

  const extensionIndex = fileName.lastIndexOf('.')
  const extension = extensionIndex > 0 ? fileName.slice(extensionIndex) : ''
  const suffixLength = clamp(extension.length + 10, 8, maxLength - 3)
  const prefixLength = Math.max(maxLength - suffixLength - 3, 1)

  return `${fileName.slice(0, prefixLength)}...${fileName.slice(-suffixLength)}`
}

export function formatProgressMessage(snapshot) {
  return (
    `progress ${formatPercent(snapshot.totalPercent)} ` +
    `file ${snapshot.currentFileIndex}/${snapshot.totalFiles} ` +
    `current ${formatPercent(snapshot.currentFilePercent)} ` +
    `speed ${snapshot.speedLabel} ` +
    `eta ${snapshot.etaLabel} ` +
    `${snapshot.transferredLabel}/${snapshot.totalLabel} ` +
    `${snapshot.displayFileName}`
  )
}

export function fitFileNameToWidth(fileName, availableWidth, fallbackMaxLength = 36) {
  if (!Number.isFinite(availableWidth)) {
    return fileName
  }

  if (availableWidth <= 0) {
    return truncateFileName(fileName, Math.max(fallbackMaxLength, 8))
  }

  if (fileName.length <= availableWidth) {
    return fileName
  }

  return truncateFileName(fileName, Math.max(Math.floor(availableWidth), 8))
}

export function formatTtyProgressLines(snapshot, barText, columns) {
  const firstLine = `upload ${barText} ${formatPercent(snapshot.totalPercent)} ${snapshot.transferredLabel}/${snapshot.totalLabel}`
  const secondLinePrefix =
    `file ${snapshot.currentFileIndex}/${snapshot.totalFiles} ` +
    `current ${formatPercent(snapshot.currentFilePercent)} ` +
    `speed ${snapshot.speedLabel} ` +
    `eta ${snapshot.etaLabel} `
  const displayFileName = fitFileNameToWidth(
    snapshot.currentFileName || snapshot.displayFileName,
    typeof columns === 'number' ? columns - secondLinePrefix.length : undefined,
  )

  return [
    firstLine,
    `${secondLinePrefix}${displayFileName}`,
  ]
}

function buildSnapshot(state, now) {
  const elapsedSeconds =
    state.startedAt === null ? 0 : Math.max((now() - state.startedAt) / 1000, 0)
  const speedBytesPerSecond =
    elapsedSeconds > 0 ? state.totalUploadedBytes / elapsedSeconds : 0
  const remainingBytes = Math.max(state.totalBytes - state.totalUploadedBytes, 0)
  const etaSeconds =
    remainingBytes === 0 ? 0 : speedBytesPerSecond > 0 ? remainingBytes / speedBytesPerSecond : null
  const totalPercent =
    state.totalBytes === 0 ? (state.finished ? 100 : 0) : (state.totalUploadedBytes / state.totalBytes) * 100
  const currentFilePercent =
    state.currentFileSize === 0
      ? state.currentFileComplete
        ? 100
        : 0
      : (state.currentFileBytes / state.currentFileSize) * 100

  return {
    currentFileBytes: state.currentFileBytes,
    currentFileIndex: state.currentFileIndex,
    currentFileName: state.currentFileName,
    currentFilePercent: clamp(currentFilePercent, 0, 100),
    currentFileSize: state.currentFileSize,
    displayFileName: state.currentFileName || '-',
    etaLabel: formatEta(etaSeconds),
    etaSeconds,
    speedBytesPerSecond,
    speedLabel: `${prettyBytes(speedBytesPerSecond)}/s`,
    totalBytes: state.totalBytes,
    totalFiles: state.totalFiles,
    totalPercent: clamp(totalPercent, 0, 100),
    totalUploadedBytes: state.totalUploadedBytes,
    totalLabel: prettyBytes(state.totalBytes),
    transferredLabel: prettyBytes(state.totalUploadedBytes),
  }
}

function createCaptureStream(columns = 120) {
  let buffer = ''

  return {
    clearBuffer() {
      buffer = ''
    },
    clearLine() {},
    columns,
    cursorTo() {
      buffer = ''
    },
    isTTY: true,
    moveCursor() {},
    write(chunk) {
      buffer += chunk
      return true
    },
    get value() {
      return buffer
    },
  }
}

export function createUploadProgressTracker({
  totalBytes,
  totalFiles,
  isTTY = Boolean(process.stdout.isTTY),
  log = console.log,
  now = () => Date.now(),
  percentStep = 5,
  ProgressBarClass = ProgressBar,
  stream = process.stdout,
  throttleMs = 1000,
} = {}) {
  const state = {
    currentFileBytes: 0,
    currentFileComplete: false,
    currentFileIndex: 0,
    currentFileName: '',
    currentFileSize: 0,
    finished: false,
    startedAt: null,
    totalBytes,
    totalFiles,
    totalUploadedBytes: 0,
  }

  let lastLoggedAt = -Infinity
  let lastLoggedBucket = -1
  let hasRendered = false

  const progressTotal = Math.max(totalBytes, 1)
  const barCaptureStream = createCaptureStream()
  const bar =
    isTTY && totalFiles > 0
      ? new ProgressBarClass(PROGRESS_BAR_FORMAT, {
          clear: false,
          complete: '=',
          incomplete: ' ',
          renderThrottle: 100,
          stream: barCaptureStream,
          total: progressTotal,
          width: PROGRESS_BAR_WIDTH,
        })
      : null

  function safeClearLine(direction = 0) {
    stream.clearLine?.(direction)
  }

  function safeCursorTo(column = 0) {
    stream.cursorTo?.(column)
  }

  function safeMoveCursor(dx = 0, dy = 0) {
    stream.moveCursor?.(dx, dy)
  }

  function getBarText(snapshot, force = false) {
    if (!bar) {
      return ''
    }

    const ratio = progressTotal > 0 ? clamp(snapshot.totalUploadedBytes / progressTotal, 0, 1) : 0
    barCaptureStream.clearBuffer()

    if (force) {
      bar.update(ratio)
      bar.render(undefined, true)
    } else {
      bar.update(ratio)
    }

    return barCaptureStream.value || bar.lastDraw || '[]'
  }

  function clearTTYRender() {
    if (!hasRendered || !stream.isTTY) {
      return
    }

    safeClearLine(0)
    safeCursorTo(0)
    safeMoveCursor(0, -1)
    safeClearLine(0)
    safeCursorTo(0)
  }

  function renderTTY(force = false) {
    const snapshot = getSnapshot()
    const barText = getBarText(snapshot, force)
    const [ firstLine, secondLine ] = formatTtyProgressLines(snapshot, barText, stream.columns)

    if (hasRendered) {
      clearTTYRender()
    }

    stream.write(firstLine)
    safeClearLine(1)
    stream.write('\n')
    stream.write(secondLine)
    safeClearLine(1)

    hasRendered = true
    return snapshot
  }

  function terminateTTYRender() {
    if (!hasRendered || !stream.isTTY) {
      return
    }

    stream.write('\n')
  }

  function ensureStarted() {
    if (state.startedAt === null) {
      state.startedAt = now()
    }
  }

  function getSnapshot() {
    return buildSnapshot(state, now)
  }

  function logSnapshot(force = false) {
    const snapshot = getSnapshot()
    const currentBucket =
      percentStep > 0 ? Math.floor(snapshot.totalPercent / percentStep) : Number.POSITIVE_INFINITY

    if (
      !force &&
      now() - lastLoggedAt < throttleMs &&
      currentBucket <= lastLoggedBucket
    ) {
      return snapshot
    }

    lastLoggedAt = now()
    lastLoggedBucket = currentBucket
    log(formatProgressMessage(snapshot))

    return snapshot
  }

  function render(force = false) {
    if (bar) {
      return renderTTY(force)
    }

    return logSnapshot(force)
  }

  function advance(deltaBytes) {
    if (deltaBytes <= 0) {
      return getSnapshot()
    }

    ensureStarted()

    const remainingFile = Math.max(state.currentFileSize - state.currentFileBytes, 0)
    const remainingTotal = Math.max(state.totalBytes - state.totalUploadedBytes, 0)
    const safeDelta = Math.min(deltaBytes, remainingFile, remainingTotal)

    if (safeDelta <= 0) {
      return getSnapshot()
    }

    state.currentFileBytes += safeDelta
    state.totalUploadedBytes += safeDelta

    return render()
  }

  function startFile(file, fileIndex) {
    ensureStarted()
    state.currentFileBytes = 0
    state.currentFileComplete = false
    state.currentFileIndex = fileIndex
    state.currentFileName = file.name
    state.currentFileSize = file.size

    return render(true)
  }

  function completeFile() {
    const remainingBytes = Math.max(state.currentFileSize - state.currentFileBytes, 0)
    if (remainingBytes > 0) {
      advance(remainingBytes)
    }

    state.currentFileComplete = true
    return render(true)
  }

  function resetCurrentFile() {
    state.totalUploadedBytes = clamp(
      state.totalUploadedBytes - state.currentFileBytes,
      0,
      Math.max(state.totalBytes, 0),
    )
    state.currentFileBytes = 0
    state.currentFileComplete = false

    return render(true)
  }

  function finish() {
    state.finished = true

    if (bar) {
      const snapshot = renderTTY(true)
      terminateTTYRender()
      return snapshot
    }

    return logSnapshot(true)
  }

  function fail(fileName = state.currentFileName) {
    const snapshot = getSnapshot()
    if (bar) {
      renderTTY(true)
      terminateTTYRender()
    }

    log(
      `upload failed at file ${snapshot.currentFileIndex}/${snapshot.totalFiles} ` +
        `${truncateFileName(fileName || snapshot.currentFileName || '-')} ` +
        `(${formatPercent(snapshot.currentFilePercent)} current, ` +
        `${formatPercent(snapshot.totalPercent)} total, ` +
        `${snapshot.speedLabel}, eta ${snapshot.etaLabel}, ` +
        `${snapshot.transferredLabel}/${snapshot.totalLabel})`,
    )
  }

  function interrupt(message) {
    if (bar && hasRendered) {
      clearTTYRender()
      stream.write(message)
      stream.write('\n')
      renderTTY(true)
      return
    }

    log(message)
  }

  return {
    advance,
    completeFile,
    fail,
    finish,
    getSnapshot,
    interrupt,
    resetCurrentFile,
    startFile,
  }
}


================================================
FILE: scripts/upload-release.mjs
================================================
import chalk from 'chalk'
import { config as loadEnv } from 'dotenv'
import { createReadStream, promises as fs } from 'node:fs'
import path from 'node:path'
import { Transform } from 'node:stream'
import { fileURLToPath } from 'node:url'
import prettyBytes from 'pretty-bytes'
import {
  getFullVersion,
  getReleaseTag,
  getReleaseVersion,
  isReleaseArtifactFile,
  resolveGithubRepository,
} from './release-config.mjs'
import {
  attachDiagnostic,
  buildDebugPayload,
  buildDiagnostic,
  formatDiagnosticSummary,
  formatRetrySummary,
} from './upload-diagnostics.mjs'
import { createUploadProgressTracker } from './upload-progress.mjs'

loadEnv()

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.normalize(path.join(__dirname, '..'))
const distDir = path.join(rootDir, 'dist')

const dryRun = process.env.DRY_RUN === '1' || process.argv.includes('--dry-run')
const token = process.env.GH_TOKEN
const repository = resolveGithubRepository(process.env)
const releaseTag = getReleaseTag(process.env)
const releaseVersion = getReleaseVersion()
const fullVersion = getFullVersion()
const retryAttempts = Math.max(
  1,
  Number.parseInt(process.env.RELEASE_UPLOAD_RETRY_ATTEMPTS, 10) || 3,
)
const retryBaseDelayMs = Math.max(
  250,
  Number.parseInt(process.env.RELEASE_UPLOAD_RETRY_BASE_DELAY_MS, 10) || 1500,
)
const retryMaxDelayMs = Math.max(
  retryBaseDelayMs,
  Number.parseInt(process.env.RELEASE_UPLOAD_RETRY_MAX_DELAY_MS, 10) || 10000,
)
const retryableStatusCodes = new Set([ 408, 409, 425, 429, 500, 502, 503, 504 ])
const debugDiagnostics = process.env.RELEASE_UPLOAD_DEBUG === '1'

function log(message) {
  console.log(`[release:upload] ${message}`)
}

function logFileList(files) {
  log('files:')
  files.forEach((file) => {
    console.log(`  - ${file.name} (${prettyBytes(file.size)})`)
  })
}

function getArtifactVersion(fileName) {
  const match = /-v(\d+\.\d+\.\d+\.\d+)-/.exec(fileName)
  return match ? match[1] : null
}

function sleep(ms) {
  return new Promise((resolve) => {
    setTimeout(resolve, ms)
  })
}

function getRetryDelayMs(attempt) {
  return Math.min(retryBaseDelayMs * 2 ** Math.max(attempt - 1, 0), retryMaxDelayMs)
}

function formatRetryDelay(ms) {
  return `${(ms / 1000).toFixed(ms >= 10000 ? 0 : 1)}s`
}

function isRetryableStatus(status) {
  return retryableStatusCodes.has(status)
}

function isRetryableFetchError(error) {
  if (!(error instanceof Error)) {
    return false
  }

  const code =
    typeof error.cause === 'object' && error.cause !== null && 'code' in error.cause
      ? String(error.cause.code || '')
      : ''
  const message = `${error.message} ${code}`.toLowerCase()

  return (
    message.includes('fetch failed') ||
    message.includes('network') ||
    message.includes('timeout') ||
    message.includes('econnreset') ||
    message.includes('eai_again') ||
    message.includes('enotfound') ||
    message.includes('econnrefused') ||
    message.includes('socket')
  )
}

function getProgressSnapshot(progressTracker) {
  return progressTracker?.getSnapshot() ?? null
}

function logDiagnosticDebug(error) {
  if (!debugDiagnostics) {
    return
  }

  const diagnostic = error instanceof Error && 'diagnostic' in error ? error.diagnostic : null
  const payload = buildDebugPayload(diagnostic, error)
  console.error(chalk.gray('[release:upload] debug diagnostic:'))
  console.error(chalk.gray(JSON.stringify(payload, null, 2)))
}

async function readReleaseFiles() {
  const entries = await fs.readdir(distDir, { withFileTypes: true })
  const files = entries.filter((entry) => entry.isFile())
  const mismatchedVersionedFiles = files
    .map((entry) => entry.name)
    .filter((fileName) => {
      const artifactVersion = getArtifactVersion(fileName)
      return artifactVersion && artifactVersion !== fullVersion
    })

  if (mismatchedVersionedFiles.length > 0) {
    throw new Error(
      `Cannot prepare GitHub Release assets for version ${fullVersion}.\n` +
        `Found old build artifacts in dist/: ${mismatchedVersionedFiles.join(', ')}\n` +
        `This usually means src/version.json was updated after the last package build, so only latest*.yml still matches.\n` +
        `Please rebuild the app for version ${fullVersion}, or clean dist/ before uploading.`,
    )
  }

  // Keep the asset picker strict so repeated uploads remain deterministic across machines.
  const selectedFiles = files
    .filter((entry) => isReleaseArtifactFile(entry.name, fullVersion))
    .map((entry) => ({
      name: entry.name,
      filePath: path.join(distDir, entry.name),
    }))
    .sort((a, b) => a.name.localeCompare(b.name))

  return Promise.all(
    selectedFiles.map(async (file) => ({
      ...file,
      size: (await fs.stat(file.filePath)).size,
    })),
  )
}

async function githubRequest(
  pathname,
  { method = 'GET', body, headers = {}, stage = 'github-request', fileName = null } = {},
) {
  const requestUrl = `https://api.github.com${pathname}`

  for (let attempt = 1; attempt <= retryAttempts; attempt += 1) {
    let response

    try {
      response = await fetch(requestUrl, {
        method,
        headers: {
          Accept: 'application/vnd.github+json',
          Authorization: `Bearer ${token}`,
          'User-Agent': 'SwitchHosts-release-uploader',
          'X-GitHub-Api-Version': '2022-11-28',
          ...headers,
        },
        body,
      })
    } catch (error) {
      const diagnostic = buildDiagnostic({
        attempt,
        error,
        fileName,
        maxAttempts: retryAttempts,
        method,
        retryable: isRetryableFetchError(error),
        stage,
        target: pathname,
      })

      if (attempt >= retryAttempts || !isRetryableFetchError(error)) {
        throw attachDiagnostic(error, diagnostic)
      }

      const delayMs = getRetryDelayMs(attempt)
      log(formatRetrySummary(diagnostic, formatRetryDelay(delayMs)))
      await sleep(delayMs)
      continue
    }

    if (!response.ok) {
      const text = await response.text()
      const error = new Error(`${method} ${pathname} failed: ${response.status} ${text}`)
      const diagnostic = buildDiagnostic({
        attempt,
        error,
        fileName,
        httpStatus: response.status,
        maxAttempts: retryAttempts,
        method,
        retryable: isRetryableStatus(response.status),
        stage,
        target: pathname,
      })

      if (attempt < retryAttempts && isRetryableStatus(response.status)) {
        const delayMs = getRetryDelayMs(attempt)
        log(formatRetrySummary(diagnostic, formatRetryDelay(delayMs)))
        await sleep(delayMs)
        continue
      }

      throw attachDiagnostic(error, diagnostic)
    }

    if (response.status === 204) {
      return null
    }

    return response.json()
  }

  throw new Error(`${method} ${pathname} failed after ${retryAttempts} attempts.`)
}

async function findReleaseByTag() {
  let page = 1
  const maxPages = 20

  while (page <= maxPages) {
    // The list API is used here because draft releases are not reliably addressable
    // through the single-release-by-tag endpoint.
    const releases = await githubRequest(
      `/repos/${repository.owner}/${repository.repo}/releases?per_page=100&page=${page}`,
      {
        stage: 'find-release',
      },
    )

    const found = releases.find((release) => release.tag_name === releaseTag)
    if (found) {
      return found
    }

    if (releases.length < 100) {
      return null
    }

    page += 1
  }
}

async function createDraftRelease() {
  return githubRequest(`/repos/${repository.owner}/${repository.repo}/releases`, {
    method: 'POST',
    stage: 'create-release',
    headers: {
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({
      tag_name: releaseTag,
      name: releaseTag,
      draft: true,
      prerelease: false,
      generate_release_notes: false,
    }),
  })
}

function getUploadUrl(release) {
  return release.upload_url.replace(/\{.*$/, '')
}

async function deleteAsset(assetId, assetName) {
  await githubRequest(`/repos/${repository.owner}/${repository.repo}/releases/assets/${assetId}`, {
    method: 'DELETE',
    stage: 'delete-asset',
    fileName: assetName,
  })
}

async function tryDeleteAssetByName(releaseId, assetName) {
  try {
    const assets = await githubRequest(
      `/repos/${repository.owner}/${repository.repo}/releases/${releaseId}/assets?per_page=100`,
      { stage: 'list-assets', fileName: assetName },
    )
    const match = assets?.find((asset) => asset.name === assetName)
    if (match) {
      await deleteAsset(match.id, assetName)
    }
  } catch (_) {
    // Best-effort cleanup — don't block the retry if this fails.
  }
}

async function uploadAsset(uploadUrl, file, { fileIndex, releaseId, progressTracker } = {}) {
  const url = new URL(uploadUrl)
  url.searchParams.set('name', file.name)
  progressTracker?.startFile(file, fileIndex)

  for (let attempt = 1; attempt <= retryAttempts; attempt += 1) {
    const fileStream = createReadStream(file.filePath)
    const trackedStream = fileStream.pipe(
      new Transform({
        transform(chunk, encoding, callback) {
          progressTracker?.advance(chunk.byteLength)
          callback(null, chunk)
        },
      }),
    )

    let response

    try {
      response = await fetch(url, {
        method: 'POST',
        headers: {
          Accept: 'application/vnd.github+json',
          Authorization: `Bearer ${token}`,
          'Content-Length': String(file.size),
          'Content-Type': 'application/octet-stream',
          'User-Agent': 'SwitchHosts-release-uploader',
          'X-GitHub-Api-Version': '2022-11-28',
        },
        body: trackedStream,
        duplex: 'half',
      })
    } catch (error) {
      fileStream.destroy()
      trackedStream.destroy()
      const diagnostic = buildDiagnostic({
        attempt,
        error,
        fileIndex,
        fileName: file.name,
        maxAttempts: retryAttempts,
        method: 'POST',
        progressSnapshot: getProgressSnapshot(progressTracker),
        retryable: isRetryableFetchError(error),
        stage: 'upload-asset',
        target: url,
      })

      if (attempt < retryAttempts && isRetryableFetchError(error)) {
        const delayMs = getRetryDelayMs(attempt)
        await tryDeleteAssetByName(releaseId, file.name)
        progressTracker?.resetCurrentFile()
        progressTracker?.interrupt(`[release:upload] ${formatRetrySummary(diagnostic, formatRetryDelay(delayMs))}`)
        await sleep(delayMs)
        continue
      }

      progressTracker?.fail(file.name)
      throw attachDiagnostic(error, diagnostic)
    }

    if (!response.ok) {
      fileStream.destroy()
      trackedStream.destroy()
      const text = await response.text()
      const error = new Error(`Upload failed for ${file.name}: ${response.status} ${text}`)
      const diagnostic = buildDiagnostic({
        attempt,
        error,
        fileIndex,
        fileName: file.name,
        httpStatus: response.status,
        maxAttempts: retryAttempts,
        method: 'POST',
        progressSnapshot: getProgressSnapshot(progressTracker),
        retryable: isRetryableStatus(response.status),
        stage: 'upload-asset',
        target: url,
      })

      if (attempt < retryAttempts && isRetryableStatus(response.status)) {
        const delayMs = getRetryDelayMs(attempt)
        await tryDeleteAssetByName(releaseId, file.name)
        progressTracker?.resetCurrentFile()
        progressTracker?.interrupt(`[release:upload] ${formatRetrySummary(diagnostic, formatRetryDelay(delayMs))}`)
        await sleep(delayMs)
        continue
      }

      progressTracker?.fail(file.name)
      throw attachDiagnostic(error, diagnostic)
    }

    progressTracker?.completeFile()
    return response.json()
  }

  const exhaustedError = new Error(`Upload failed for ${file.name} after ${retryAttempts} attempts.`)
  progressTracker?.fail(file.name)
  throw attachDiagnostic(
    exhaustedError,
    buildDiagnostic({
      attempt: retryAttempts,
      error: exhaustedError,
      fileIndex,
      fileName: file.name,
      maxAttempts: retryAttempts,
      method: 'POST',
      progressSnapshot: getProgressSnapshot(progressTracker),
      retryable: false,
      stage: 'upload-asset',
      target: url,
    }),
  )
}

async function main() {
  const files = await readReleaseFiles()
  const totalFiles = files.length
  const totalBytes = files.reduce((sum, file) => sum + file.size, 0)

  if (files.length === 0) {
    throw new Error(`No release artifacts found in ${distDir} for version ${fullVersion}.`)
  }

  log(`repository: ${repository.fullName}`)
  log(`release version: ${releaseVersion}`)
  log(`release tag: ${releaseTag}`)
  log(`artifacts: ${totalFiles} files, ${prettyBytes(totalBytes)}`)
  logFileList(files)

  if (dryRun) {
    log('dry run enabled, skipping GitHub API calls.')
    return
  }

  if (!token) {
    throw new Error('GH_TOKEN is required unless DRY_RUN=1 is set.')
  }

  let release = await findReleaseByTag()
  if (!release) {
    log(`release ${releaseTag} not found, creating draft release...`)
    release = await createDraftRelease()
  } else {
    log(`using existing release ${releaseTag} (draft=${release.draft}, prerelease=${release.prerelease})`)
  }

  const uploadUrl = getUploadUrl(release)
  const existingAssets = new Map(release.assets.map((asset) => [asset.name, asset]))
  const progressTracker = createUploadProgressTracker({
    totalBytes,
    totalFiles,
    log,
  })
  const logUploadStatus = (message) => progressTracker.interrupt(`[release:upload] ${message}`)

  for (const [ index, file ] of files.entries()) {
    const existingAsset = existingAssets.get(file.name)
    if (existingAsset) {
      // Replace same-name assets so different machines can safely append
      // or refresh artifacts for the same draft release.
      logUploadStatus(`replacing existing asset ${file.name}`)
      await deleteAsset(existingAsset.id, file.name)
    } else {
      logUploadStatus(`uploading new asset ${file.name}`)
    }

    await uploadAsset(uploadUrl, file, {
      fileIndex: index + 1,
      releaseId: release.id,
      progressTracker,
    })
  }

  progressTracker.finish()
  log(`done: ${release.html_url}`)
}

try {
  await main()
} catch (error) {
  const diagnostic = error instanceof Error && 'diagnostic' in error ? error.diagnostic : null
  const message = diagnostic ? formatDiagnosticSummary(diagnostic) : error instanceof Error ? error.message : String(error)
  console.error(chalk.red(`[release:upload] ${message}`))
  logDiagnosticDebug(error)
  process.exit(1)
}


================================================
FILE: scripts/vars.mjs
================================================
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.normalize(path.join(__dirname, '..'))
const distDir = path.normalize(path.join(__dirname, '..', 'dist'))

const APP_NAME = 'SwitchHosts'

const electronLanguages = ['en', 'fr', 'zh_CN', 'de', 'ja', 'tr', 'ko']

export { APP_NAME, distDir, electronLanguages, rootDir }


================================================
FILE: scripts/version-up.mjs
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import fs from 'node:fs'
import path from 'node:path'
import { fileURLToPath } from 'node:url'

const __filename = fileURLToPath(import.meta.url)
const __dirname = path.dirname(__filename)
const rootDir = path.dirname(__dirname)
const versionFile = path.join(rootDir, 'src', 'version.json')
const appPackageFile = path.join(rootDir, 'app', 'package.json')
const version = JSON.parse(fs.readFileSync(versionFile, 'utf8'))
const appPackage = JSON.parse(fs.readFileSync(appPackageFile, 'utf8'))

const versionInc = (v) => {
  return ++v
}

version[3] = versionInc(version[3])

console.log(`version -> ${version.slice(0, 3).join('.')}(${version[3]})`)
fs.writeFileSync(versionFile, `[${version.join(', ')}]`)

appPackage.version = version.slice(0, 3).join('.') + '.' + version[3]
fs.writeFileSync(
  appPackageFile,
  JSON.stringify(appPackage, null, 2),
  'utf8',
)


================================================
FILE: src/common/acknowledgements.ts
================================================
/**
 * acknowledgements
 * @author: oldj
 * @homepage: https://oldj.net
 */

export default [
  { name: 'oldj', link: 'https://github.com/oldj' },
  { name: 'Allen.M', link: 'https://github.com/allenm' },
  { name: 'Charles Tang', link: 'https://github.com/charlestang' },
  { name: 'WuJianjun', link: 'https://github.com/stotem' },
  { name: 'Elf Sundae', link: 'https://github.com/ElfSundae' },
  { name: 'zhu yu', link: 'https://github.com/codeyu' },
  { name: '胖梁', link: 'https://github.com/pangliang' },
  { name: 'CaffreySun', link: 'https://github.com/CaffreySun' },
  { name: 'Xmader', link: 'https://github.com/Xmader' },
  { name: 'Dean Zhang', link: 'https://github.com/zhanggang807' },
  { name: 'CloverNet', link: 'https://github.com/CloverNet' },
  { name: 'ReAlign', link: 'https://github.com/ReAlign' },
  { name: 'Kangyi Cui', link: 'https://github.com/cuikangyi' },
  { name: 'AKIRA', link: 'https://github.com/akrha' },
  { name: 'Constaline', link: 'https://github.com/Constaline' },
  { name: 'TooBug', link: 'https://github.com/TooBug' },
  { name: 'Lussac', link: 'https://github.com/LussacZheng' },
  { name: 'Aktilor', link: 'https://github.com/Aktilor' },
  { name: 'LiangLong', link: 'https://github.com/xxccll' },
  { name: 'ClDaniel1', link: 'https://github.com/ClDaniel1' },
  { name: 'Aaron Xie', link: 'https://github.com/Aaron00101010' },
  { name: 'Stefan Berger', link: 'https://github.com/bergo' },
  { name: 'EmeryWan', link: 'https://github.com/EmeryWan' },
  { name: 'ClDaniel1', link: 'https://github.com/ClDaniel1' },
  { name: 'moonheart', link: 'https://github.com/moonheart' },
  { name: 'Wang Weitao', link: 'https://github.com/watonyweng' },
  { name: 'kamatte', link: 'https://github.com/kamatte-me' },
  { name: 'Yuyao Nie', link: 'https://github.com/nieyuyao' },
  { name: 'Xav83', link: 'https://github.com/Xav83' },
  { name: 'Mango Jelly Pudding', link: 'https://github.com/EvanHsieh0415' },
  { name: 'Alex Zappa', link: 'https://github.com/reatlat' },
  { name: 'shenshen', link: 'https://github.com/imshenshen' },
  { name: 'ChunRen Zhang', link: 'https://github.com/rayatn1011' },
  { name: 'Barış Uzun', link: 'https://github.com/barisuzunn' },
  { name: 'Hwang In-wook', link: 'https://github.com/wooklab' },
]


================================================
FILE: src/common/constants.ts
================================================
/**
 * constants
 * @author: oldj
 * @homepage: https://oldj.net
 */

export const server_url = 'https://switchhosts.vercel.app'
export const homepage_url = `${server_url}/home/`
export const download_url = `${server_url}/download/`
export const source_url = 'https://github.com/oldj/SwitchHosts'
export const feedback_url = 'https://github.com/oldj/SwitchHosts/issues'
export const http_api_port = 50761


================================================
FILE: src/common/data.d.ts
================================================
import { ITreeNodeData } from './tree'

export type HostsType = 'local' | 'remote' | 'group' | 'folder'
export type FolderModeType = 0 | 1 | 2 // 0: 默认; 1: 单选; 2: 多选

export interface IHostsListObject {
  id: string
  title?: string
  on?: boolean
  type?: HostsType

  // remote
  url?: string
  last_refresh?: string
  last_refresh_ms?: number
  refresh_interval?: number // 单位:秒

  // group
  include?: string[]

  // folder
  folder_mode?: FolderModeType
  folder_open?: boolean
  children?: IHostsListObject[]

  is_sys?: boolean

  [key: string]: any
}

export interface IHostsContentObject {
  id: string
  content: string

  [key: string]: any
}

export interface ITrashcanObject {
  data: IHostsListObject
  add_time_ms: number
  parent_id: string | null
}

export interface ITrashcanListObject extends ITrashcanObject, ITreeNodeData {
  id: string
  children?: ITrashcanListObject[]
  is_root?: boolean
  type?: HostsType | 'trashcan'

  [key: string]: any
}

export interface IHostsHistoryObject {
  id: string
  content: string
  add_time_ms: number
  label?: string
}

export type VersionType = [number, number, number, number]

export interface IHostsBasicData {
  list: IHostsListObject[]
  trashcan: ITrashcanObject[]
  version: VersionType
}

export interface IOperationResult {
  success: boolean
  message?: string
  data?: any
  code?: string | number
}

export interface ICommandRunResult {
  _id?: string
  success: boolean
  stdout: string
  stderr: string
  add_time_ms: number
}


================================================
FILE: src/common/default_configs.ts
================================================
import { LocaleName } from '@common/i18n'
import { FolderModeType } from './data.d'

export type WriteModeType = null | 'overwrite' | 'append'
export type ThemeType = 'light' | 'dark' | 'system'
export type ProtocolType = 'http' | 'https'
export type DefaultLocaleType = LocaleName | undefined

const configs = {
  // UI
  left_panel_show: true,
  left_panel_width: 270,
  use_system_window_frame: false,

  // preferences
  write_mode: 'append' as WriteModeType,
  history_limit: 50,
  locale: undefined as DefaultLocaleType,
  theme: 'light' as ThemeType,
  choice_mode: 2 as FolderModeType,
  show_title_on_tray: false,
  hide_at_launch: false,
  send_usage_data: false,
  cmd_after_hosts_apply: '',
  remove_duplicate_records: false,
  hide_dock_icon: false,
  use_proxy: false,
  proxy_protocol: 'http' as ProtocolType,
  proxy_host: '',
  proxy_port: 0,
  http_api_on: false,
  http_api_only_local: true,
  tray_mini_window: true,
  multi_chose_folder_switch_all: false,

  // Legacy key: it now controls background update checks, while the actual
  // download remains a manual action in the UI.
  auto_download_update: true,

  // other
  env: 'PROD' as 'PROD' | 'DEV',
}

export type ConfigsType = typeof configs

export default configs


================================================
FILE: src/common/events.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

export default {
  active_main_window: 'active_main_window',
  add_new: 'add_new',
  browser_link: 'browser_link',
  close_find: 'close_find',
  cmd_run_result: 'cmd_run_result',
  config_updated: 'config_updated',
  edit_hosts_info: 'edit_hosts_info',
  hosts_content_changed: 'hosts_content_changed',
  hosts_refreshed: 'hosts_refreshed',
  hosts_refreshed_by_id: 'hosts_refreshed_by_id',
  move_to_trashcan: 'move_to_trashcan',
  new_version: 'new_version',
  reload_list: 'reload_list',
  select_hosts: 'select_hosts',
  set_hosts_on_status: 'set_hosts_on_status',
  show_about: 'show_about',
  show_history: 'show_history',
  show_preferences: 'show_preferences',
  show_set_write_mode: 'show_set_write_mode',
  show_source: 'show_source',
  show_sudo_password_input: 'show_sudo_password_input',
  system_hosts_updated: 'system_hosts_updated',
  toggle_comment: 'toggle_comment',
  toggle_developer_tools: 'toggle_developer_tools',
  toggle_item: 'toggle_item',
  toggle_left_panel: 'toggle_left_panel',
  tray_list_updated: 'tray:list_updated',
  update_download_progress: 'update_download_progress',
  update_downloaded: 'update_downloaded',
  write_hosts_to_system: 'write_hosts_to_system',
}


================================================
FILE: src/common/hostsFn.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { FolderModeType, IHostsBasicData, IHostsListObject } from '@common/data'
import lodash from 'lodash'

type PartHostsObjectType = Partial<IHostsListObject> & { id: string }

type Predicate = (obj: IHostsListObject) => boolean

export const flatten = (list: IHostsListObject[]): IHostsListObject[] => {
  let new_list: IHostsListObject[] = []

  list.map((item) => {
    new_list.push(item)
    if (item.children) {
      new_list = [...new_list, ...flatten(item.children)]
    }
  })

  return new_list
}

export const cleanHostsList = (data: IHostsBasicData): IHostsBasicData => {
  let list = flatten(data.list)

  list.map((item) => {
    if (item.type === 'folder' && !Array.isArray(item.children)) {
      item.children = [] as IHostsListObject[]
    }

    if (item.type === 'group' && !Array.isArray(item.include)) {
      item.include = [] as string[]
    }

    if (item.type === 'folder' || item.type === 'group') {
      item.content = ''
    }
  })

  return data
}

export const findItemById = (
  list: IHostsListObject[],
  id: string,
): IHostsListObject | undefined => {
  return flatten(list).find((item) => item.id === id)
}

export const updateOneItem = (
  list: IHostsListObject[],
  item: PartHostsObjectType,
): IHostsListObject[] => {
  let new_list: IHostsListObject[] = lodash.cloneDeep(list)

  let i = findItemById(new_list, item.id)
  if (i) {
    Object.assign(i, item)
  }

  return new_list
}

const isInTopLevel = (list: IHostsListObject[], id: string): boolean => {
  return list.findIndex((i) => i.id === id) > -1
}

export const setOnStateOfItem = (
  list: IHostsListObject[],
  id: string,
  on: boolean,
  default_choice_mode: FolderModeType = 0,
  multi_chose_folder_switch_all: boolean = false,
): IHostsListObject[] => {
  let new_list: IHostsListObject[] = lodash.cloneDeep(list)

  let item = findItemById(new_list, id)
  if (!item) return new_list

  item.on = on

  let itemIsInTopLevel = isInTopLevel(list, id)
  if (multi_chose_folder_switch_all) {
    item = switchFolderChild(item, on)
    !itemIsInTopLevel && switchItemParentIsON(new_list, item, on)
  }

  if (!on) {
    return new_list
  }

  if (itemIsInTopLevel) {
    if (default_choice_mode === 1) {
      new_list.map((item) => {
        if (item.id !== id) {
          item.on = false
          if (multi_chose_folder_switch_all) {
            item = switchFolderChild(item, false)
          }
        }
      })
    }
  } else {
    let parent = getParentOfItem(new_list, id)
    if (parent) {
      let folder_mode = parent.folder_mode || default_choice_mode
      if (folder_mode === 1 && parent.children) {
        // 单选模式
        parent.children.map((item) => {
          if (item.id !== id) {
            item.on = false
            if (multi_chose_folder_switch_all) {
              item = switchFolderChild(item, false)
            }
          }
        })
      }
    }
  }

  return new_list
}

export const switchItemParentIsON = (
  list: IHostsListObject[],
  item: IHostsListObject,
  on: boolean,
) => {
  let parent = getParentOfItem(list, item.id)

  if (parent) {
    if (parent.folder_mode === 1) {
      return
    }
    if (!on) {
      parent.on = on
    } else if (parent.children) {
      let parentOn = true
      parent.children.forEach((item) => {
        if (!item.on) {
          parentOn = false
        }
      })
      parent.on = parentOn
    }

    let itemIsInTopLevel = isInTopLevel(list, parent.id)
    if (!itemIsInTopLevel) {
      switchItemParentIsON(list, parent, on)
    }
  }
}

export const switchFolderChild = (item: IHostsListObject, on: boolean): IHostsListObject => {
  if (item.type != 'folder') {
    return item
  }
  let folder_mode = item.folder_mode
  if (folder_mode === 1) {
    return item
  }

  if (item.children) {
    item.children.forEach((item) => {
      item.on = on
      if (item.type == 'folder') {
        item = switchFolderChild(item, on)
      }
    })
  }

  return item
}

export const deleteItemById = (list: IHostsListObject[], id: string) => {
  let idx = list.findIndex((item) => item.id === id)
  if (idx >= 0) {
    list.splice(idx, 1)
    return
  }

  list.map((item) => deleteItemById(item.children || [], id))
}

// export const getNextSelectedItem = (list: IHostsListObject[], id: string): IHostsListObject | undefined => {
//   let flat = flatten(list)
//   let idx = flat.findIndex(item => item.id === id)
//
//   return flat[idx + 1] || flat[idx - 1]
// }

export const getNextSelectedItem = (
  tree: IHostsListObject[],
  predicate: Predicate,
): IHostsListObject | undefined => {
  let flat = flatten(tree)
  let idx_1 = -1
  let idx_2 = -1

  flat.map((i, idx) => {
    if (predicate(i)) {
      if (idx_1 === -1) {
        idx_1 = idx
      }
      idx_2 = idx
    }
  })

  return flat[idx_2 + 1] || flat[idx_1 - 1]
}

export const getParentOfItem = (
  list: IHostsListObject[],
  item_id: string,
): IHostsListObject | undefined => {
  if (list.find((i) => i.id === item_id)) {
    // is in the top level
    return
  }

  let flat = flatten(list)
  for (let p of flat) {
    if (p.children && p.children.find((i) => i.id === item_id)) {
      return p
    }
  }
}


================================================
FILE: src/common/i18n/index.ts
================================================
/**
 * index
 * @author: oldj
 * @homepage: https://oldj.net
 */

import en from './languages/en'
import zh from './languages/zh'
import zh_hant from './languages/zh-hant'
import fr from './languages/fr'
import de from './languages/de'
import ja from './languages/ja'
import tr from './languages/tr'
import ko from './languages/ko'
import pl from './languages/pl'
import { LanguageDict, LanguageKey } from '@common/types'

export const languages = {
  en,
  zh,
  cn: zh,
  'zh-CN': zh,
  zh_hant: zh_hant,
  'zh-TW': zh_hant,
  fr,
  de,
  ja,
  tr,
  ko,
  pl,
}

export type LocaleName = keyof typeof languages

export class I18N {
  locale: LocaleName
  lang: LanguageDict

  constructor(locale: LocaleName = 'en') {
    this.locale = locale

    const _this = this

    this.lang = new Proxy(
      {},
      {
        get(obj, key: LanguageKey) {
          return _this.trans(key)
        },
      },
    ) as LanguageDict
  }

  trans(key: LanguageKey, words?: string[]) {
    let lang = languages[this.locale]

    let s: string = ''

    if (key in lang) {
      s = lang[key].toString()
    }

    if (words) {
      words.map((w, idx) => {
        let reg = new RegExp(`\{\s*${idx}\s*}`)
        s = s.replace(reg, w)
      })
    }

    return s
  }
}


================================================
FILE: src/common/i18n/languages/de.ts
================================================
/**
 * @author: bergo
 * @homepage: https://bergo.dev
 */

import { LanguageDict } from '@common/types'

const lang: LanguageDict = {
  _app_name: 'SwitchHosts',
  _key: 'de',
  _name: 'Deutsch',
  about: 'Über',
  acknowledgement: 'Danksagung',
  advanced: 'Erweitert',
  all: 'Alle',
  append: 'Anhängen',
  auto_refresh: 'Automatisch aktualisieren',
  btn_cancel: 'Abbrechen',
  btn_ok: 'OK',
  change: 'Ändern',
  check_update: 'Aktualisierung prüfen',
  choice_mode: 'Auswahlmodus',
  choice_mode_default: 'Standard',
  choice_mode_desc:
    'Gilt nur für das oberste Element, jeder Ordner kann seinen eigenen Auswahlmodus festlegen.',
  choice_mode_multiple: 'Mehrfach',
  choice_mode_single: 'Einfach',
  choices: 'Auswahlen',
  chosen: 'Ausgewählt',
  clear_history: 'Verlauf löschen',
  click_to_open: 'Klicken zum Öffnen',
  close: 'Schließen',
  colon: ': ',
  commands: 'Befehle',
  commands_help: 'Die folgenden Systembefehle werden ausgeführt, wenn Hosts angewendet werden:',
  commands_title: 'Befehl nach dem Anlegen eines Hosts',
  comment_current_line: 'Aktuelle Zeile kommentieren',
  content: 'Inhalt',
  copy: 'Kopieren',
  cut: 'Ausschneiden',
  day: 'Tag',
  days: 'Tage',
  delete: 'Löschen',
  download: 'Herunterladen',
  edit: 'Bearbeiten',
  export: 'Exportieren',
  export_done: 'Der Export ist abgeschlossen.',
  fail: 'Fehlgeschlagen!',
  feedback: 'Rückmeldung',
  file: 'Datei',
  find: 'Suchen',
  find_all: 'Alles suchen',
  find_and_replace: 'Suchen und ersetzen',
  find_history: 'Verlauf suchen',
  folder: 'Ordner',
  front: 'Vorderseite',
  general: 'Allgemein',
  group: 'Gruppe',
  help: 'Hilfe',
  hide: 'Ausblenden',
  hide_at_launch: 'Beim Start ausblenden',
  hide_dock_icon: 'Dock-Symbol ausblenden',
  hide_history: 'Verlauf ausblenden',
  hide_others: 'Andere ausblenden',
  homepage: 'Startseite',
  host: 'Gastgeber',
  hosts_add: 'Neue Hosts hinzufügen',
  hosts_delete: 'Diesen Host löschen',
  hosts_delete_confirm: 'Sind Sie sicher, dass Sie die aktuellen Hosts löschen wollen?',
  hosts_edit: 'Hosts bearbeiten',
  hosts_title: 'Titel des Hosts',
  hosts_type: 'Hosts Typ',
  hosts_updated: 'Die Hosts-Datei wurde aktualisiert.',
  hour: 'Stunde',
  hours: 'Stunden',
  http_api_on: 'HTTP-API eingeschaltet',
  http_api_on_desc:
    'Läuft auf Port {0}, kann von Software von Drittanbietern wie Alfred verwendet werden, um den Host zu wechseln.',
  http_api_only_local: 'HTTP-API hört nur auf 127.0.0.1',
  ignore_case: 'Groß- und Kleinschreibung ignorieren',
  import: 'Importieren',
  import_done: 'Der Import ist abgeschlossen.',
  import_fail: 'Der Import ist fehlgeschlagen!',
  import_from_url: 'Importieren von URL',
  is_latest_version_inform: 'Super, Sie haben die neueste Version!',
  check_update_failed: 'Suche nach Updates fehlgeschlagen!',
  update_download_now: 'Update herunterladen',
  update_install_now: 'Installieren und neu starten',
  update_downloading_desc: 'Version {0} wird heruntergeladen: {1}',
  update_ready_desc: 'Version {0} wurde heruntergeladen und kann jetzt installiert werden.',
  item_found: '{0} Einträge gefunden.',
  items: 'items',
  items_found: '{0} Einträge gefunden.',
  language: 'Sprache',
  last_refresh: 'Letzte Aktualisierung: ',
  latest_version_desc: 'Die neueste Version ist: {0}',
  line: 'Zeile',
  lines: 'Zeilen',
  loading: 'Loading...',
  local: 'Lokal',
  match: 'Übereinstimmung',
  migrate_confirm:
    'SwitchHosts v4.0 verwendet ein neues Datenspeicherformat, möchten Sie alte Daten in das neue Format migrieren?',
  migrate_data: 'Daten migrieren',
  minimize: 'Minimieren',
  minute: 'Minute',
  minutes: 'Minuten',
  move_items_to_trashcan: 'Verschiebe {0} Objekte in den Mülleimer',
  move_to_trashcan: 'In die Mülltonne verschieben',
  multi_chose_folder_switch_all: 'Mehrfachauswahl-Ordnerschalter zur Steuerung von Unterelementen',
  need_to_relaunch: 'Muss neu gestartet werden',
  need_to_relaunch_after_setting_changed:
    'Die Einstellungen wurden geändert und werden erst nach einem Neustart der App wirksam.',
  never: 'Niemals',
  new: 'Neu',
  new_version_found: 'Neue Version gefunden',
  next: 'Nächste',
  no_access_to_hosts: 'Keine Berechtigung zum Schreiben in die Hosts-Datei.',
  no_record: 'Kein Datensatz',
  overwrite: 'Überschreiben',
  password: 'Passwort',
  paste: 'Einfügen',
  port: 'Anschluss',
  preferences: 'Präferenzen',
  previous: 'Vorhergehend',
  protocol: 'Protokoll',
  proxy: 'Proxy',
  quit: 'Beenden',
  read_only: 'Nur Lesen',
  redo: 'Wiederherstellen',
  refresh: 'Auffrischen',
  regexp: 'Regulärer Ausdruck',
  reload: 'Neu laden',
  remote: 'Entfernt',
  remove_duplicate_records: 'Doppelte Datensätze entfernen',
  remove_duplicate_records_desc:
    'Wenn eine Domain auf mehrere IPs verweist, wird nur die erste wirksam, die folgenden werden in Kommentare umgewandelt.',
  replace: 'Ersetzen',
  replace_all: 'Ersetze alle',
  replace_history: 'Historie ersetzen',
  reset: 'Zurücksetzen',
  reset_data_dir_confirm:
    'Sind Sie sicher, dass Sie den Datenordner an der Standardadresse ({0}) wiederherstellen wollen?',
  reset_zoom: 'Zoom zurücksetzen',
  search: 'Suchen',
  select_all: 'Alles auswählen',
  selected: 'Ausgewählt',
  show_dock_icon: 'Dock-Symbol anzeigen',
  show_history: 'Historie anzeigen',
  show_main_window: 'Hauptfenster anzeigen',
  show_title_on_tray: 'Titel auf dem Tablett anzeigen',
  source_code: 'Quellcode',
  success: 'Erfolg!',
  sudo_prompt_title: 'Geben Sie Ihr sudo-Passwort ein',
  system_hosts: 'System-Hosts',
  system_hosts_history: 'Historische Versionen der System-Hosts',
  system_hosts_history_delete_confirm: 'Sind Sie sicher, dass Sie dieses Element löschen wollen?',
  system_hosts_history_help:
    'Wenn die Gesamtzahl der historischen Einträge diese Grenze überschreitet, wird der älteste Eintrag gelöscht.',
  system_hosts_history_limit: 'Maximale Anzahl von Datensätzen: ',
  test: 'Test',
  theme: 'Thema',
  theme_dark: 'Dunkel',
  theme_light: 'Hell',
  title: 'Titel',
  to_show_source: 'Durch Doppelklick wird der Quellcode angezeigt.',
  toggle_developer_tools: 'Entwicklerwerkzeuge einschalten',
  toggle_dock_icon: 'Das Dock-Symbol einschalten',
  toggle_full_screen: 'Vollbildmodus einschalten',
  trashcan: 'Mülleimer',
  trashcan_clear: 'Den Mülleimer leeren',
  trashcan_clear_confirm: 'Sind Sie sicher, dass Sie den Mülleimer leeren wollen?',
  trashcan_delete_confirm: 'Möchten Sie dieses Objekt vollständig löschen?',
  trashcan_restore: 'Wiederherstellen',
  tray_mini_window: 'Taskleistensymbol-Verknüpfung',
  undo: 'Rückgängig machen',
  unhide: 'Einblenden',
  untitled: 'Ohne Titel',
  url_placeholder: 'http:// oder https:// oder file://',
  usage_data_agree: 'Ja, übermitteln Sie anonymisierte Nutzungsdaten',
  usage_data_help:
    'Möchten Sie uns helfen, SwitchHosts zu verbessern, indem Sie regelmäßig anonyme Nutzungsdaten übermitteln?',
  usage_data_title: 'Machen Sie SwitchHosts besser!',
  use_proxy: 'Proxy verwenden',
  use_system_window_frame:
    'Verwenden Sie den Systemfensterrahmen, ein Neustart der Anwendung ist erforderlich',
  view: 'Ansicht',
  where_is_my_data: 'Wo sind meine Daten gespeichert?',
  where_is_my_hosts: 'Wo ist meine Hosts-Datei?',
  window: 'Fenster',
  write_mode: 'Schreibmodus',
  write_mode_append_help:
    'Hängen Sie die neuen Datensätze an das Ende der Hosts-Datei des Systems an.',
  write_mode_overwrite_help: 'Überschreibt die Systemhosts-Datei mit den neuen Datensätzen.',
  write_mode_set: 'Schreibmodus einstellen',
  your_data_is: 'Ihre Datendateien sind gespeichert in:',
  your_hosts_file_is: 'Ihre Hosts-Datei befindet sich in:',
  zoom: 'Vergrößern',
  zoom_in: 'Vergrößern',
  zoom_out: 'Herauszoomen',
}

export default lang


================================================
FILE: src/common/i18n/languages/en.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

export default {
  _app_name: 'SwitchHosts',
  _key: 'en',
  _name: 'English',
  about: 'About',
  acknowledgement: 'Acknowledgement',
  advanced: 'Advanced',
  all: 'All',
  append: 'Append',
  auto_refresh: 'Auto refresh',
  btn_cancel: 'Cancel',
  btn_ok: 'OK',
  change: 'Change',
  check_update: 'Check update',
  choice_mode: 'Choice mode',
  choice_mode_default: 'Default',
  choice_mode_desc:
    'Only valid for the topmost item, each folder can set its own choice mode.',
  choice_mode_multiple: 'Multiple',
  choice_mode_single: 'Single',
  choices: 'Choices',
  chosen: 'Chosen',
  clear_history: 'Clear history',
  click_to_open: 'Click to open',
  close: 'Close',
  colon: ': ',
  commands: 'Commands',
  commands_help:
    'The following system commands will be executed when Hosts applied:',
  commands_title: 'Command after hosts are applied',
  comment_current_line: 'Comment current line',
  content: 'Content',
  copy: 'Copy',
  cut: 'Cut',
  day: 'day',
  days: 'days',
  delete: 'Delete',
  download: 'Download',
  edit: 'Edit',
  export: 'Export',
  export_done: 'The export is complete.',
  fail: 'Fail!',
  feedback: 'Feedback',
  file: 'File',
  find: 'Find',
  find_all: 'Find all',
  find_and_replace: 'Find and replace',
  find_history: 'Find history',
  folder: 'Folder',
  front: 'Front',
  general: 'General',
  group: 'Group',
  help: 'Help',
  hide: 'Hide',
  hide_at_launch: 'Hide at launch',
  hide_dock_icon: 'Hide the dock icon',
  hide_history: 'Hide history',
  hide_others: 'Hide others',
  homepage: 'Homepage',
  host: 'Host',
  hosts_add: 'Add new hosts',
  hosts_delete: 'Delete this hosts',
  hosts_delete_confirm: 'Are you sure you want to delete the current hosts?',
  hosts_edit: 'Edit hosts',
  hosts_title: 'Hosts title',
  hosts_type: 'Hosts type',
  hosts_updated: 'The Hosts file has been updated.',
  hour: 'hour',
  hours: 'hours',
  http_api_on: 'HTTP API on',
  http_api_on_desc:
    'Runs on port {0}, can be used by third-party software such as Alfred to switch hosts.',
  http_api_only_local: 'HTTP API only listen 127.0.0.1',
  ignore_case: 'Ignore case',
  import: 'Import',
  import_done: 'The import is complete.',
  import_fail: 'Import failed!',
  import_from_url: 'Import from URL',
  is_latest_version_inform: 'Great, you are running the latest version!',
  check_update_failed: 'Check for updates failed!',
  update_download_now: 'Download update',
  update_install_now: 'Install and restart',
  update_downloading_desc: 'Downloading version {0}: {1}',
  update_ready_desc: 'Version {0} has been downloaded and is ready to install.',
  item_found: '{0} item found.',
  items: 'items',
  items_found: '{0} items found.',
  language: 'Language',
  last_refresh: 'Last refresh: ',
  latest_version_desc: 'The latest version is: {0}',
  line: 'line',
  lines: 'lines',
  loading: 'Loading...',
  local: 'Local',
  match: 'Match',
  migrate_confirm:
    'SwitchHosts v4.0 uses a new data storage format, do you want to migrate old data to the new format?',
  migrate_data: 'Migrate data',
  minimize: 'Minimize',
  minute: 'minute',
  minutes: 'minutes',
  move_items_to_trashcan: 'Move {0} items to trashcan',
  move_to_trashcan: 'Move to trashcan',
  multi_chose_folder_switch_all:
    'multi-select folder switch to control sub-items',
  need_to_relaunch: 'Need to relaunch',
  need_to_relaunch_after_setting_changed:
    'The setting has been changed and will take effect after the app is restarted.',
  never: 'Never',
  new: 'New',
  new_version_found: 'New version found',
  next: 'Next',
  no_access_to_hosts: 'No permission to write to the Hosts file.',
  no_record: 'No record',
  overwrite: 'Overwrite',
  password: 'Password',
  paste: 'Paste',
  port: 'Port',
  preferences: 'Preferences',
  previous: 'Previous',
  protocol: 'Protocol',
  proxy: 'Proxy',
  quit: 'Quit',
  read_only: 'Read only',
  redo: 'Redo',
  refresh: 'Refresh',
  regexp: 'Regular expression',
  reload: 'Reload',
  remote: 'Remote',
  remove_duplicate_records: 'Remove duplicate records',
  remove_duplicate_records_desc:
    'If a domain points to multiple IPs, only the first one will take effect, and the following ones will be converted into comments.',
  replace: 'Replace',
  replace_all: 'Replace all',
  replace_history: 'Replace history',
  reset: 'Reset',
  reset_data_dir_confirm:
    'Are you sure you want to restore the data folder to the default address ({0})?',
  reset_zoom: 'Reset zoom',
  search: 'Search',
  select_all: 'Select all',
  selected: 'Selected',
  show_dock_icon: 'Show the dock icon',
  show_history: 'Show history',
  show_main_window: 'Show main window',
  show_title_on_tray: 'Show title on tray',
  source_code: 'Souce code',
  success: 'Success!',
  sudo_prompt_title: 'Input your sudo password',
  system_hosts: 'System Hosts',
  system_hosts_history: 'History versions of the System Hosts',
  system_hosts_history_delete_confirm:
    'Are you sure you want to delete this item?',
  system_hosts_history_help:
    'If the total number of historical records exceeds this limit, the oldest record will be deleted.',
  system_hosts_history_limit: 'Maximum number of records: ',
  test: 'Test',
  theme: 'Theme',
  theme_dark: 'Dark',
  theme_light: 'Light',
  title: 'Title',
  to_show_source: 'Double-click to show the source code.',
  toggle_developer_tools: 'Toggle Developer Tools',
  toggle_dock_icon: 'Toggle the dock icon',
  toggle_full_screen: 'Toggle full screen',
  trashcan: 'Trashcan',
  trashcan_clear: 'Empty the trashcan',
  trashcan_clear_confirm: 'Are you sure you want to empty the trashcan?',
  trashcan_delete_confirm: 'Do you want to delete this item completely?',
  trashcan_restore: 'Restore',
  tray_mini_window: 'taskbar icon shortcut',
  undo: 'Undo',
  unhide: 'Unhide',
  untitled: 'Untitled',
  url_placeholder: 'http:// or https:// or file://',
  usage_data_agree: 'Yes, submit anonymized usage data',
  usage_data_help:
    'Would you like to help us improve SwitchHosts by periodically submitting anonymous usage data?',
  usage_data_title: 'Make SwitchHosts better!',
  use_proxy: 'Use proxy',
  use_system_window_frame:
    'Use system window frame, application restart is required',
  view: 'View',
  where_is_my_data: 'Where is my data stored?',
  where_is_my_hosts: 'Where is my hosts file?',
  window: 'Window',
  write_mode: 'Write mode',
  write_mode_append_help:
    'Append the new records to the end of the system hosts file.',
  write_mode_overwrite_help:
    'Overwrite the system hosts file with the new records.',
  write_mode_set: 'Set the write mode',
  your_data_is: 'Your data files are stored in:',
  your_hosts_file_is: 'Your hosts file is located at:',
  zoom: 'Zoom',
  zoom_in: 'Zoom in',
  zoom_out: 'Zoom out',
}


================================================
FILE: src/common/i18n/languages/fr.ts
================================================
/**
 * @author: Aktilor
 * @homepage: https://github.com/Aktilor
 */

import { LanguageDict } from '@common/types'

const lang: LanguageDict = {
  _app_name: 'SwitchHosts',
  _key: 'fr',
  _name: 'Français',
  about: 'À propos',
  acknowledgement: 'Remerciements',
  advanced: 'Avancé',
  all: 'Tout',
  append: 'Ajouter',
  auto_refresh: 'Rafraîchissement automatique',
  btn_cancel: 'Annuler',
  btn_ok: 'OK',
  change: 'Changer',
  check_update: 'Vérifier les mises à jour',
  choice_mode: 'Choice mode',
  choice_mode_default: 'Défaut',
  choice_mode_desc:
    "Uniquement valable pour l'élément le plus haut, chaque dossier peut définir son propre mode.",
  choice_mode_multiple: 'Multiple',
  choice_mode_single: 'Seul',
  choices: 'Choix',
  chosen: 'Choisi',
  clear_history: "Effacer l'historique",
  click_to_open: 'Cliquer pour ouvrir',
  close: 'Fermer',
  colon: ' : ',
  commands: 'Commandes',
  commands_help: "Les commandes systèmes suivantes seront exécutées quand l'hosts sera activé :",
  commands_title: "Commandes une fois qu'un hosts est activé",
  comment_current_line: 'Commenter cette ligne',
  content: 'Contenu',
  copy: 'Copier',
  cut: 'Couper',
  day: 'jour',
  days: 'jours',
  delete: 'Supprimer',
  download: 'Télécharger',
  edit: 'Éditer',
  export: 'Exporter',
  export_done: "L'export est terminé.",
  fail: 'Échec !',
  feedback: 'Laisser un commentaire',
  file: 'Fichier',
  find: 'Rechercher',
  find_all: 'Rechercher tout',
  find_and_replace: 'Rechercher et remplacer',
  find_history: 'Historique des recherches',
  folder: 'Dossier',
  front: 'Front',
  general: 'Général',
  group: 'Groupe',
  help: 'Aide',
  hide: 'Cacher',
  hide_at_launch: 'Cacher au lancement',
  hide_dock_icon: "Cacher l'icone dans le Dock",
  hide_history: "Cacher l'historique",
  hide_others: 'Cacher les autres',
  homepage: "Page d'accueil",
  host: 'Host',
  hosts_add: 'Ajouter un nouvel hosts',
  hosts_delete: 'Supprimer cet hosts',
  hosts_delete_confirm: 'Êtes-vous sûr de vouloir supprimer cet hosts?',
  hosts_edit: "Éditer l'hosts",
  hosts_title: "Titre de l'hosts",
  hosts_type: "Type d'hosts",
  hosts_updated: 'Le fichier hosts a été mis à jour',
  hour: 'heure',
  hours: 'heures',
  http_api_on: 'Activer HTTP API',
  http_api_on_desc:
    "Actif sur le port {0}, peut être utilisé par un logiciel tier comme Alfred pour changer d'hosts",
  http_api_only_local: "L'API HTTP n'écoute que sur 127.0.0.1",
  ignore_case: 'Ignorer la casse',
  import: 'Importer',
  import_done: "L'importation est terminée",
  import_fail: "Échec de l'importation !",
  import_from_url: "Importer à partir d'une URL",
  is_latest_version_inform: 'Super, vous avez la dernière version !',
  check_update_failed: 'La vérification des mises à jour a échoué !',
  update_download_now: 'Télécharger la mise à jour',
  update_install_now: 'Installer et redémarrer',
  update_downloading_desc: 'Téléchargement de la version {0} : {1}',
  update_ready_desc: 'La version {0} a été téléchargée et est prête à être installée.',
  item_found: '{0} élément trouvé.',
  items: 'éléments',
  items_found: '{0} éléments trouvés.',
  language: 'Langage',
  last_refresh: 'Dernier rafraîchissement : ',
  latest_version_desc: 'La dernière version est : {0}',
  line: 'ligne',
  lines: 'lignes',
  loading: 'Chargement...',
  local: 'Local',
  match: 'Correspondance',
  migrate_confirm:
    'SwitchHosts v4.0 utilise un nouveau format de stockage des données, voulez-vous migrer les anciennes données dans ce nouveau format ?',
  migrate_data: 'Migrer les données',
  minimize: 'Réduire',
  minute: 'minute',
  minutes: 'minutes',
  move_items_to_trashcan: 'Déplacer {0} éléments dans la corbeille',
  move_to_trashcan: 'Déplacer dans la corbeille',
  multi_chose_folder_switch_all:
    'Commutateur de dossier à sélection multiple pour contrôler les sous-éléments',
  need_to_relaunch: 'Besoin de redémarrer',
  need_to_relaunch_after_setting_changed:
    "Le paramètre a été modifié et prendra effet après le redémarrage de l'application.",
  never: 'Jamais',
  new: 'Nouveau',
  new_version_found: 'Nouvelle version trouvée',
  next: 'Suivant',
  no_access_to_hosts: 'Aucune autorisation pour écrire dans le fichier hosts.',
  no_record: 'Aucun enregistrement',
  overwrite: 'Écraser',
  password: 'Mot de passe',
  paste: 'Coller',
  port: 'Port',
  preferences: 'Préférences',
  previous: 'Précédent',
  protocol: 'Protocol',
  proxy: 'Proxy',
  quit: 'Quitter',
  read_only: 'Lecture seule',
  redo: 'Rétablir',
  refresh: 'Rafraîchir',
  regexp: 'Expression régulière',
  reload: 'Recharger',
  remote: 'Distant',
  remove_duplicate_records: 'Supprimer les enregistrements doublons',
  remove_duplicate_records_desc:
    'Si un domaine pointe sur plusieurs IPs, seulement la première sera prise en compte, et les autres seront converties en commentaires.',
  replace: 'Remplacer',
  replace_all: 'Tout remplacer',
  replace_history: "Remplacer l'historique",
  reset: 'Réinitialiser',
  reset_data_dir_confirm:
    "Êtes-vous sûr de vouloir réinitialiser le dossier de données à l'adresse par défaut?({0})?",
  reset_zoom: 'Réinitialiser le zoom',
  search: 'Rechercher',
  select_all: 'Tout sélectionner',
  selected: 'Sélectionné',
  show_dock_icon: "Afficher l'icone dans le Dock",
  show_history: "Afficher l'historique",
  show_main_window: 'Afficher la fenêtre principale',
  show_title_on_tray: 'Afficher le titre dans la barre des menus',
  source_code: 'Code source',
  success: 'Succès !',
  sudo_prompt_title: 'Entrez votre mot de passe sudo',
  system_hosts: 'Hosts du système',
  system_hosts_history: 'Historique des versions hosts du système',
  system_hosts_history_delete_confirm: 'Êtes-vous sûr de vouloir supprimer cet élément ?',
  system_hosts_history_help:
    "Si le nombre total d'enregistrements dépasse cette limite, l'enregistrement le plus ancien sera supprimé.",
  system_hosts_history_limit: "Nombre max. d'enregistrements : ",
  test: 'Test',
  theme: 'Thème',
  theme_dark: 'Sombre',
  theme_light: 'Clair',
  title: 'Titre',
  to_show_source: 'Double-cliquez pour afficher le code source',
  toggle_developer_tools: 'Afficher/Cacher le Developer Tools',
  toggle_dock_icon: "Afficher/Cacher l'icone dans le Dock",
  toggle_full_screen: 'Activer/Désactiver le plein écran',
  trashcan: 'Corbeille',
  trashcan_clear: 'Vider la corbeille',
  trashcan_clear_confirm: 'Êtes-vous sûr de vouloir vider la corbeille ?',
  trashcan_delete_confirm: 'Voulez-vous supprimer définitivement cet élément ?',
  trashcan_restore: 'Restaurer',
  tray_mini_window: "raccourci de l'icône de la barre des tâches",
  undo: 'Annuler',
  unhide: 'Démasquer',
  untitled: 'Sans titre',
  url_placeholder: 'http:// ou https:// ou file://',
  usage_data_agree: "Oui, soumettre de manière anonyme mes données d'utilisation",
  usage_data_help:
    "Voulez-vous nous aider à améliorer SwitchHosts en soumettant périodiquement vos données d'utilisation de manière anonyme ?",
  usage_data_title: 'Rendez SwitchHosts meilleur !',
  use_proxy: 'Utiliser un proxy',
  use_system_window_frame:
    "Utiliser le cadre de la fenêtre système, le redémarrage de l'application est requis",
  view: 'Vue',
  where_is_my_data: 'Où sont stockées mes données ?',
  where_is_my_hosts: 'Où est mon fichier hosts ?',
  window: 'Fenêtre',
  write_mode: "Mode d'écriture",
  write_mode_append_help:
    "Ajoutez les nouveaux enregistrements à la fin du fichier d'hôtes système.",
  write_mode_overwrite_help:
    "Écrasez le fichier d'hôtes système avec les nouveaux enregistrements.",
  write_mode_set: "Définir le mode d'écriture",
  your_data_is: 'Les fichiers contenant vos données sont stockés ici :',
  your_hosts_file_is: 'Votre fichier hosts est situé ici :',
  zoom: 'Zoom',
  zoom_in: 'Zoommer',
  zoom_out: 'Dézoommer',
}

export default lang


================================================
FILE: src/common/i18n/languages/ja.ts
================================================
/**
 * @author: kamatte
 * @homepage: https://kamatte.me
 */

import { LanguageDict } from '@common/types'

const lang: LanguageDict = {
  _app_name: 'SwitchHosts',
  _key: 'ja',
  _name: '日本語',
  about: 'SwitchHosts について',
  acknowledgement: '謝辞',
  advanced: '詳細設定',
  all: 'すべて',
  append: '追記',
  auto_refresh: '自動更新',
  btn_cancel: 'キャンセル',
  btn_ok: 'OK',
  change: '変更',
  check_update: 'アップデートを確認',
  choice_mode: '選択モード',
  choice_mode_default: 'デフォルト',
  choice_mode_desc:
    '最上位階層のhostsにのみ有効で、各フォルダーでは独自に選択モードを設定できます。',
  choice_mode_multiple: '複数',
  choice_mode_single: '単一',
  choices: '選択',
  chosen: '選択済み',
  clear_history: '履歴をクリア',
  click_to_open: 'クリックして開く',
  close: '閉じる',
  colon: ': ',
  commands: 'コマンド',
  commands_help: 'hostsが適用されたとき、以下のシステムコマンドを実行します:',
  commands_title: 'hosts適用後のコマンド',
  comment_current_line: '現在の行をコメントアウト',
  content: '内容',
  copy: 'コピー',
  cut: '切り取り',
  day: '日',
  days: '日',
  delete: '削除',
  download: 'ダウンロード',
  edit: '編集',
  export: 'エクスポート',
  export_done: 'エクスポートが完了しました。',
  fail: '失敗',
  feedback: 'フィードバック',
  file: 'ファイル',
  find: '検索',
  find_all: 'すべて検索',
  find_and_replace: '検索と置換',
  find_history: '検索履歴',
  folder: 'フォルダー',
  front: '前面',
  general: '一般',
  group: 'グループ',
  help: 'ヘルプ',
  hide: '非表示',
  hide_at_launch: '起動時に非表示',
  hide_dock_icon: 'Dockアイコンを非表示',
  hide_history: '履歴を非表示',
  hide_others: 'その他を非表示にする',
  homepage: 'ホームページ',
  host: 'ホスト',
  hosts_add: 'hostsを追加',
  hosts_delete: 'hostsを削除',
  hosts_delete_confirm: 'このhostsを削除してもよろしいですか?',
  hosts_edit: 'hostsを編集',
  hosts_title: 'hostsタイトル',
  hosts_type: 'hostsタイプ',
  hosts_updated: 'hostsを更新しました。',
  hour: '時間',
  hours: '時間',
  http_api_on: 'HTTP APIを有効化',
  http_api_on_desc:
    '{0}番ポートで実行され、Alfredなどのサードパーティソフトウェアでhostsを切り替えるために使用できます。',
  http_api_only_local: 'HTTP APIを 127.0.0.1 のみでリッスンする',
  ignore_case: '大文字と小文字を区別しない',
  import: 'インポート',
  import_done: 'インポートが完了しました。',
  import_fail: 'インポートに失敗しました!',
  import_from_url: 'URLからインポート',
  is_latest_version_inform: 'ご利用のバージョンは最新です!',
  check_update_failed: 'アップデートの確認に失敗しました!',
  update_download_now: '更新をダウンロード',
  update_install_now: 'インストールして再起動',
  update_downloading_desc: 'バージョン {0} をダウンロード中: {1}',
  update_ready_desc: 'バージョン {0} のダウンロードが完了し、インストールできます。',
  item_found: '{0}件見つかりました。',
  items: '件',
  items_found: '{0}件見つかりました。',
  language: '言語',
  last_refresh: '最終更新: ',
  latest_version_desc: '最新バージョン: {0}',
  line: '行',
  lines: '行',
  loading: '読み込み中...',
  local: 'ローカル',
  match: '一致',
  migrate_confirm:
    'SwitchHosts v4.0は新しいデータ保存形式を使用します。古いデータを新しい形式に移行しますか?',
  migrate_data: 'データ移行',
  minimize: '最小化',
  minute: '分',
  minutes: '分',
  move_items_to_trashcan: '{0}件をごみ箱に入れる',
  move_to_trashcan: 'ゴミ箱に入れる',
  multi_chose_folder_switch_all: 'フォルダーの切り替えで配下のアイテムを一括操作',
  need_to_relaunch: '再起動が必要です',
  need_to_relaunch_after_setting_changed:
    '変更された設定はアプリケーションの再起動後に有効になります。',
  never: 'なし',
  new: '新規',
  new_version_found: '新しいバージョンが見つかりました',
  next: '次へ',
  no_access_to_hosts: 'hostsファイルの書き込み権限がありません。',
  no_record: 'なし',
  overwrite: '上書き',
  password: 'パスワード',
  paste: '貼り付け',
  port: 'ポート',
  preferences: '設定',
  previous: '前へ',
  protocol: 'プロトコル',
  proxy: 'プロキシ',
  quit: '終了',
  read_only: '読み取り専用',
  redo: 'やり直し',
  refresh: '更新',
  regexp: '正規表現',
  reload: '再読み込み',
  remote: 'リモート',
  remove_duplicate_records: '重複レコードを削除',
  remove_duplicate_records_desc:
    '1つのドメインに複数のIPアドレスを指定している場合、先頭のIPアドレスのみが有効になり、以降のIPアドレスはコメントに変換されます。',
  replace: '置換',
  replace_all: 'すべて置換',
  replace_history: '置換履歴',
  reset: 'リセット',
  reset_data_dir_confirm: 'データフォルダーの場所をデフォルト ({0}) に戻してもよろしいですか?',
  reset_zoom: 'ズームをリセット',
  search: '検索',
  select_all: 'すべて選択',
  selected: '選択済み',
  show_dock_icon: 'Dockアイコンを表示',
  show_history: '履歴を表示',
  show_main_window: 'メインウィンドウを表示',
  show_title_on_tray: 'トレイにタイトルを表示',
  source_code: 'ソースコード',
  success: '成功',
  sudo_prompt_title: '管理者パスワードを入力してください',
  system_hosts: 'システムhosts',
  system_hosts_history: 'システムhostsのバージョン履歴',
  system_hosts_history_delete_confirm: 'この履歴を削除してもよろしいですか?',
  system_hosts_history_help: '履歴件数がこれを超えると、最も古い履歴が削除されます。',
  system_hosts_history_limit: '履歴の最大件数: ',
  test: 'テスト',
  theme: 'テーマ',
  theme_dark: 'ダーク',
  theme_light: 'ライト',
  title: 'タイトル',
  to_show_source: 'ダブルクリックでソースコードを表示する。',
  toggle_developer_tools: '開発者ツールの表示/非表示',
  toggle_dock_icon: 'Dockアイコンの表示/非表示',
  toggle_full_screen: 'フルスクリーン',
  trashcan: 'ゴミ箱',
  trashcan_clear: 'ゴミ箱を空にする',
  trashcan_clear_confirm: 'ゴミ箱を空にしてもよろしいですか?',
  trashcan_delete_confirm: 'この項目を完全に削除しますか?',
  trashcan_restore: '戻す',
  tray_mini_window: 'タスクバーアイコンショートカット',
  undo: '元に戻す',
  unhide: 'すべて表示',
  untitled: '無題',
  url_placeholder: 'http:// または https:// または file://',
  usage_data_agree: 'はい、匿名の利用データを送信します。',
  usage_data_help: '匿名の利用データを定期的に送信し、SwitchHostsの改善にご協力いただけませんか?',
  usage_data_title: 'SwitchHostsの改善に協力する',
  use_proxy: 'プロキシを使用',
  use_system_window_frame:
    'システムのウィンドウフレームを使用。アプリケーションの再起動が必要です。',
  view: '表示',
  where_is_my_data: 'データはどこに保存されますか?',
  where_is_my_hosts: 'hostsファイルはどこにありますか?',
  window: 'ウィンドウ',
  write_mode: '書き込みモード',
  write_mode_append_help: '新しいレコードをシステムhostsの末尾に追記します。',
  write_mode_overwrite_help: '新しいレコードでシステムhostsを上書きします。',
  write_mode_set: '書き込みモードを設定',
  your_data_is: 'あなたのデータファイルはこちらに保存されています:',
  your_hosts_file_is: 'あなたのhostsファイルはこちらにあります:',
  zoom: 'ズーム',
  zoom_in: '拡大',
  zoom_out: '縮小',
}

export default lang


================================================
FILE: src/common/i18n/languages/ko.ts
================================================
/**
 * @author: wooklab
 */

export default {
  _app_name: 'SwitchHosts',
  _key: 'ko',
  _name: '한국어',
  about: '정보',
  acknowledgement: '승인',
  advanced: '고급',
  all: '전체',
  append: '추가',
  auto_refresh: '자동 새로고침',
  btn_cancel: '취소',
  btn_ok: '확인',
  change: '수정',
  check_update: '업데이트 확인',
  choice_mode: '선택 모드',
  choice_mode_default: '기본값',
  choice_mode_desc:
    '최상위 항목에만 유효하며, 각 폴더는 고유의 선택 모드를 설정할 수 있습니다.',
  choice_mode_multiple: '다중모드',
  choice_mode_single: '단일모드',
  choices: '선택',
  chosen: '선택됨',
  clear_history: '이력 삭제',
  click_to_open: '클릭하여 열기',
  close: '닫기',
  colon: ': ',
  commands: '명령어',
  commands_help:
    '호스트를 적용하면 시스템에 명령어가 실행됩니다:',
  commands_title: '호스트가 적용된 후 명령어',
  comment_current_line: '주석 현재 줄',
  content: '내용',
  copy: '복사',
  cut: '자르기',
  day: '일',
  days: '일',
  delete: '삭제',
  download: '다운로드',
  edit: '수정',
  export: '내보내기',
  export_done: '내보내기가 완료되었습니다.',
  fail: '실패!',
  feedback: '피드백',
  file: '파일',
  find: '찾기',
  find_all: '전체 찾기',
  find_and_replace: '찾기 및 바꾸기',
  find_history: '이력 찾기',
  folder: '폴더',
  front: '앞쪽',
  general: '일반',
  group: '그룹',
  help: '도움말',
  hide: '숨기기',
  hide_at_launch: '시작 시 숨기기',
  hide_dock_icon: '독(Dock) 아이콘 숨기기',
  hide_history: '이력 숨기기',
  hide_others: '다른 것 숨기기',
  homepage: '홈페이지',
  host: '호스트',
  hosts_add: '새로운 호스트 추가',
  hosts_delete: '이 호스트 삭제',
  hosts_delete_confirm: '현재 호스트를 삭제하시겠습니까?',
  hosts_edit: '호스트 수정',
  hosts_title: '호스트 제목',
  hosts_type: '호스트 유형',
  hosts_updated: '호스트 파일이 갱신되었습니다.',
  hour: '시간',
  hours: '시간',
  http_api_on: 'HTTP API 사용',
  http_api_on_desc:
    '포트 {0}에서 실행되며, Alfred와 같은 서드파티 소프트웨어를 통해 호스트를 전환하는데 사용할 수 있습니다.',
  http_api_only_local: 'HTTP API 127.0.0.1만 수신',
  ignore_case: '대소문자 무시',
  import: '가져오기',
  import_done: '가져오기가 완료되었습니다.',
  import_fail: '가져오기 실패!',
  import_from_url: 'URL에서 가져오기',
  is_latest_version_inform: '좋아요, 최신 버전을 실행 중입니다.!',
  check_update_failed: '업데이트 확인에 실패했습니다!',
  update_download_now: '업데이트 다운로드',
  update_install_now: '설치 후 다시 시작',
  update_downloading_desc: '버전 {0} 다운로드 중: {1}',
  update_ready_desc: '버전 {0} 다운로드가 완료되었으며 설치할 수 있습니다.',
  item_found: '{0}개의 항목을 찾았습니다.',
  items: '항목',
  items_found: '{0}개의 항목들을 찾았습니다.',
  language: '언어',
  last_refresh: '마지막 새로고침: ',
  latest_version_desc: '최신버전: {0}',
  line: '줄',
  lines: '줄들',
  loading: '로딩중...',
  local: '로컬',
  match: '일치',
  migrate_confirm:
    'SwitchHosts v4.0는 새로운 데이터 저장 형식을 사용합니다. 이전 데이터를 새 형식으로 마이그레이션(이전)하시겠습니까?',
  migrate_data: '데이터 이전',
  minimize: '최소화',
  minute: '분',
  minutes: '분',
  move_items_to_trashcan: '{0}개의 항목이 휴지통으로 이동',
  move_to_trashcan: '휴지통으로 이동',
  multi_chose_folder_switch_all:
    '하위 항목을 제어할 다중 선택 폴더 스위치',
  need_to_relaunch: '다시 시작해야 함',
  need_to_relaunch_after_setting_changed:
    '설정이 변경되었으며 앱을 다시 시작한 후에 적용됩니다.',
  never: '절대',
  new: '신규',
  new_version_found: '새 버전을 찾았습니다',
  next: '다음',
  no_access_to_hosts: '호스트 파일 쓰기 권한이 없습니다.',
  no_record: '레코드 없음',
  overwrite: '덮어쓰기',
  password: '패스워드',
  paste: '붙여넣기',
  port: '포트',
  preferences: '설정',
  previous: '이전',
  protocol: '프로토콜',
  proxy: '프록시',
  quit: '종료',
  read_only: '읽기 전용',
  redo: '재실행',
  refresh: '새로고침',
  regexp: '정규식',
  reload: '새로고침',
  remote: '리모트',
  remove_duplicate_records: '중복 레코드 삭제',
  remove_duplicate_records_desc:
    '만약 도메인이 여러 IP를 가리키는 경우, 첫 번째 IP만 적용되며, 나머지는 주석처리됩니다.',
  replace: '대체하기',
  replace_all: '전체 대체하기',
  replace_history: '이력 교체',
  reset: '재설정',
  reset_data_dir_confirm:
    '이 데이터 폴더를 기본 경로({0})로 복원하시겠습니까?',
  reset_zoom: '확대 재설정',
  search: '찾기',
  select_all: '전체 선택',
  selected: '선택',
  show_dock_icon: '독(Dock) 아이콘 보기',
  show_history: '이력 보기',
  show_main_window: '메인 창 보기',
  show_title_on_tray: '트레이에 제목 표시',
  source_code: '소스코드',
  success: '성공!',
  sudo_prompt_title: 'sudo password 입력',
  system_hosts: '시스템 호스트',
  system_hosts_history: '시스템 호스트의 이력버전',
  system_hosts_history_delete_confirm:
    '이 항목을 삭제하시겠습니까?',
  system_hosts_history_help:
    '이력 개수가 이 제한을 초과하면 가장 오래된 이력부터 삭제됩니다.',
  system_hosts_history_limit: '이력 최대 개수: ',
  test: '테스트',
  theme: '테마',
  theme_dark: '다크',
  theme_light: '라이트',
  title: '제목',
  to_show_source: '더블 클릭하여 소스코드를 표시합니다.',
  toggle_developer_tools: '개발자 도구 전환',
  toggle_dock_icon: '독(DocK) 아이콘 전환',
  toggle_full_screen: '전체화면 전환',
  trashcan: '휴지통',
  trashcan_clear: '휴지통 비우기',
  trashcan_clear_confirm: '휴지통을 비우시겠습니까?',
  trashcan_delete_confirm: '이 항목을 완전히 삭제하시겠습니까?',
  trashcan_restore: '복구',
  tray_mini_window: '작업 표시줄 아이콘 바로가기',
  undo: '실행취소',
  unhide: '숨김해제',
  untitled: '제목없음',
  url_placeholder: 'http:// or https:// or file://',
  usage_data_agree: '익명화된 사용 데이터 제출에 동의합니다',
  usage_data_help:
    '주기적으로 익명의 사용 데이터를 제출하여 SwitchHost를 개선하는 데 도움을 주시겠습니까?',
  usage_data_title: 'SwitchHosts개선!',
  use_proxy: '프록시 사용',
  use_system_window_frame:
    '시스템 창을 사용하려면, 프로그램 재시작이 필요합니다',
  view: '뷰',
  where_is_my_data: '내 데이터는 어디에 저장되나요?',
  where_is_my_hosts: '내 호스트 파일은 어디에 있나요?',
  window: '창',
  write_mode: '쓰기 모드',
  write_mode_append_help:
    '시스템 호스트 파일 마지막 줄에 새 레코드를 추가합니다.',
  write_mode_overwrite_help:
    '시스템 호스트 파일에 새 레코드로 덮어씁니다.',
  write_mode_set: '쓰기 모드 설정',
  your_data_is: '데이터 저장 위치:',
  your_hosts_file_is: '호스트 파일 위치:',
  zoom: '확대',
  zoom_in: '확대',
  zoom_out: '축소',
}


================================================
FILE: src/common/i18n/languages/pl.ts
================================================
/**
 * @author: piteriuz
 * @homepage: https://piotr.pienkowski.pl/
 */

export default {
  _app_name: 'SwitchHosts',
  _key: 'pl',
  _name: 'Polski',
  about: 'O aplikacji',
  acknowledgement: 'Podziękowania',
  advanced: 'Zaawansowane',
  all: 'Wszystko',
  append: 'Dołącz',
  auto_refresh: 'Automatyczne odświeżanie',
  btn_cancel: 'Anuluj',
  btn_ok: 'OK',
  change: 'Zmień',
  check_update: 'Sprawdź aktualizacje',
  choice_mode: 'Tryb wyboru',
  choice_mode_default: 'Domyślny',
  choice_mode_desc: 'Obowiązuje tylko dla elementu na górze, każdy folder może mieć własny tryb wyboru.',
  choice_mode_multiple: 'Wiele',
  choice_mode_single: 'Jeden',
  choices: 'Wybory',
  chosen: 'Wybrane',
  clear_history: 'Wyczyść historię',
  click_to_open: 'Kliknij, aby otworzyć',
  close: 'Zamknij',
  colon: ': ',
  commands: 'Polecenia',
  commands_help: 'Poniższe polecenia systemowe będą wykonane po zastosowaniu Hosts:',
  commands_title: 'Polecenie po zastosowaniu hosts',
  comment_current_line: 'Skomentuj bieżącą linię',
  content: 'Zawartość',
  copy: 'Kopiuj',
  cut: 'Wytnij',
  day: 'dzień',
  days: 'dni',
  delete: 'Usuń',
  download: 'Pobierz',
  edit: 'Edytuj',
  export: 'Eksportuj',
  export_done: 'Eksport został ukończony.',
  fail: 'Błąd!',
  feedback: 'Opinia',
  file: 'Plik',
  find: 'Znajdź',
  find_all: 'Znajdź wszystkie',
  find_and_replace: 'Znajdź i zamień',
  find_history: 'Historia wyszukiwania',
  folder: 'Folder',
  front: 'Przód',
  general: 'Ogólne',
  group: 'Grupa',
  help: 'Pomoc',
  hide: 'Ukryj',
  hide_at_launch: 'Ukryj przy uruchomieniu',
  hide_dock_icon: 'Ukryj ikonę docka',
  hide_history: 'Ukryj historię',
  hide_others: 'Ukryj inne',
  homepage: 'Strona główna',
  host: 'Host',
  hosts_add: 'Dodaj nowe hosty',
  hosts_delete: 'Usuń ten hosts',
  hosts_delete_confirm: 'Czy na pewno chcesz usunąć bieżące hosty?',
  hosts_edit: 'Edytuj hosty',
  hosts_title: 'Nazwa hosts',
  hosts_type: 'Typ hosts',
  hosts_updated: 'Plik Hosts został zaktualizowany.',
  hour: 'godzina',
  hours: 'godziny',
  http_api_on: 'HTTP API włączone',
  http_api_on_desc: 'Działa na porcie {0}, może być używane przez oprogramowanie stron trzecich, takie jak Alfred do przełączania hostów.',
  http_api_only_local: 'HTTP API nasłuchuje tylko na 127.0.0.1',
  ignore_case: 'Ignoruj wielkość liter',
  import: 'Importuj',
  import_done: 'Import został ukończony.',
  import_fail: 'Import nie powiódł się!',
  import_from_url: 'Importuj z adresu URL',
  is_latest_version_inform: 'Świetnie, masz najnowszą wersję!',
  check_update_failed: 'Sprawdzanie aktualizacji nie powiodło się!',
  update_download_now: 'Pobierz aktualizację',
  update_install_now: 'Zainstaluj i uruchom ponownie',
  update_downloading_desc: 'Pobieranie wersji {0}: {1}',
  update_ready_desc: 'Wersja {0} została pobrana i jest gotowa do instalacji.',
  item_found: 'Znaleziono {0} element.',
  items: 'elementy',
  items_found: 'Znaleziono {0} elementów.',
  language: 'Język',
  last_refresh: 'Ostatnie odświeżenie: ',
  latest_version_desc: 'Najnowsza wersja to: {0}',
  line: 'linia',
  lines: 'linie',
  loading: 'Ładowanie...',
  local: 'Lokalny',
  match: 'Dopasuj',
  migrate_confirm: 'SwitchHosts v4.0 używa nowego formatu przechowywania danych, czy chcesz migrować stare dane do nowego formatu?',
  migrate_data: 'Migruj dane',
  minimize: 'Minimalizuj',
  minute: 'minuta',
  minutes: 'minuty',
  move_items_to_trashcan: 'Przenieś {0} elementy do kosza',
  move_to_trashcan: 'Przenieś do kosza',
  multi_chose_folder_switch_all: 'wielokrotny wybór folderu do kontroli podelementów',
  need_to_relaunch: 'Wymagane ponowne uruchomienie',
  need_to_relaunch_after_setting_changed: 'Ustawienie zostało zmienione i wejdzie w życie po ponownym uruchomieniu aplikacji.',
  never: 'Nigdy',
  new: 'Nowy',
  new_version_found: 'Znaleziono nową wersję',
  next: 'Dalej',
  no_access_to_hosts: 'Brak uprawnień do zapisu w pliku Hosts.',
  no_record: 'Brak rekordu',
  overwrite: 'Nadpisz',
  password: 'Hasło',
  paste: 'Wklej',
  port: 'Port',
  preferences: 'Preferencje',
  previous: 'Wstecz',
  protocol: 'Protokół',
  proxy: 'Proxy',
  quit: 'Zamknij',
  read_only: 'Tylko do odczytu',
  redo: 'Powtórz',
  refresh: 'Odśwież',
  regexp: 'Wyrażenie regularne',
  reload: 'Załaduj ponownie',
  remote: 'Zdalny',
  remove_duplicate_records: 'Usuń zduplikowane rekordy',
  remove_duplicate_records_desc: 'Jeśli domena wskazuje na wiele adresów IP, tylko pierwszy będzie obowiązywać, a pozostałe zostaną skonwertowane na komentarze.',
  replace: 'Zamień',
  replace_all: 'Zamień wszystko',
  replace_history: 'Historia zamiany',
  reset: 'Resetuj',
  reset_data_dir_confirm: 'Czy na pewno chcesz przywrócić folder danych do adresu domyślnego ({0})?',
  reset_zoom: 'Resetuj powiększenie',
  search: 'Szukaj',
  select_all: 'Zaznacz wszystko',
  selected: 'Wybrane',
  show_dock_icon: 'Pokaż ikonę docka',
  show_history: 'Pokaż historię',
  show_main_window: 'Pokaż główne okno',
  show_title_on_tray: 'Pokaż tytuł na pasku zadań',
  source_code: 'Kod źródłowy',
  success: 'Sukces!',
  sudo_prompt_title: 'Wpisz swoje hasło sudo',
  system_hosts: 'System Hosts',
  system_hosts_history: 'Historyczne wersje System Hosts',
  system_hosts_history_delete_confirm: 'Czy na pewno chcesz usunąć ten element?',
  system_hosts_history_help: 'Jeśli całkowita liczba rekordów historycznych przekroczy ten limit, najstarszy rekord zostanie usunięty.',
  system_hosts_history_limit: 'Maksymalna liczba rekordów: ',
  test: 'Test',
  theme: 'Motyw',
  theme_dark: 'Ciemny',
  theme_light: 'Jasny',
  title: 'Tytuł',
  to_show_source: 'Kliknij dwukrotnie, aby wyświetlić kod źródłowy.',
  toggle_developer_tools: 'Przełącz narzędzia deweloperskie',
  toggle_dock_icon: 'Przełącz ikonę docka',
  toggle_full_screen: 'Przełącz pełny ekran',
  trashcan: 'Kosz',
  trashcan_clear: 'Opróżnij kosz',
  trashcan_clear_confirm: 'Czy na pewno chcesz opróżnić kosz?',
  trashcan_delete_confirm: 'Czy chcesz całkowicie usunąć ten element?',
  trashcan_restore: 'Przywróć',
  tray_mini_window: 'skrót ikony paska zadań',
  undo: 'Cofnij',
  unhide: 'Pokaż',
  untitled: 'Bez tytułu',
  url_placeholder: 'http:// lub https:// lub file://',
  usage_data_agree: 'Tak, prześlij anonimowe dane użytkowania',
  usage_data_help: 'Czy chcesz nam pomóc ulepszyć SwitchHosts, okresowo przesyłając anonimowe dane użytkowania?',
  usage_data_title: 'Uczynić SwitchHosts lepszym!',
  use_proxy: 'Użyj proxy',
  use_system_window_frame: 'Używaj systemowych ramek okna, wymagane ponowne uruchomienie aplikacji',
  view: 'Widok',
  where_is_my_data: 'Gdzie są przechowywane moje dane?',
  where_is_my_hosts: 'Gdzie znajduje się mój plik hosts?',
  window: 'Okno',
  write_mode: 'Tryb zapisu',
  write_mode_append_help: 'Dołącz nowe rekordy na koniec systemowego pliku hosts.',
  write_mode_overwrite_help: 'Nadpisz plik hosts systemu nowymi rekordami.',
  write_mode_set: 'Ustaw tryb zapisu',
  your_data_is: 'Twoje pliki danych są przechowywane w:',
  your_hosts_file_is: 'Twój plik hosts znajduje się w:',
  zoom: 'Powiększenie',
  zoom_in: 'Powiększ',
  zoom_out: 'Pomniejsz',
}


================================================
FILE: src/common/i18n/languages/tr.ts
================================================
/**
 * @author: baris
 * @homepage: https://barisuzun.com.tr
 */

export default {
  _app_name: 'SwitchHosts',
  _key: 'tr',
  _name: 'Türkçe',
  about: 'Hakkında',
  acknowledgement: 'Teşekkür',
  advanced: 'Gelişmiş',
  all: 'Tümü',
  append: 'Ekle',
  auto_refresh: 'Otomatik Yenile',
  btn_cancel: 'İptal',
  btn_ok: 'Tamam',
  change: 'Değiştir',
  check_update: 'Güncellemeleri Kontrol Et',
  choice_mode: 'Seçim Modu',
  choice_mode_default: 'Varsayılan',
  choice_mode_desc: 'Sadece en üstteki öğe için geçerlidir, her klasör kendi seçim modunu ayarlayabilir.',
  choice_mode_multiple: 'Çoklu',
  choice_mode_single: 'Tekli',
  choices: 'Seçenekler',
  chosen: 'Seçildi',
  clear_history: 'Geçmişi Temizle',
  click_to_open: 'Açmak için tıkla',
  close: 'Kapat',
  colon: ': ',
  commands: 'Komutlar',
  commands_help: 'Hosts uygulandığında aşağıdaki sistem komutları çalıştırılacaktır:',
  commands_title: 'Hosts uygulandıktan sonra komut',
  comment_current_line: 'Mevcut satırı yorumla',
  content: 'İçerik',
  copy: 'Kopyala',
  cut: 'Kes',
  day: 'gün',
  days: 'günler',
  delete: 'Sil',
  download: 'İndir',
  edit: 'Düzenle',
  export: 'Dışa Aktar',
  export_done: 'Dışa aktarma tamamlandı.',
  fail: 'Başarısız!',
  feedback: 'Geri Bildirim',
  file: 'Dosya',
  find: 'Bul',
  find_all: 'Hepsini Bul',
  find_and_replace: 'Bul ve Değiştir',
  find_history: 'Arama Geçmişi',
  folder: 'Klasör',
  front: 'Ön',
  general: 'Genel',
  group: 'Grup',
  help: 'Yardım',
  hide: 'Gizle',
  hide_at_launch: 'Başlangıçta Gizle',
  hide_dock_icon: 'Dock simgesini gizle',
  hide_history: 'Geçmişi Gizle',
  hide_others: 'Diğerlerini Gizle',
  homepage: 'Anasayfa',
  host: 'Host',
  hosts_add: 'Yeni host ekle',
  hosts_delete: 'Bu hostu sil',
  hosts_delete_confirm: 'Mevcut hostu silmek istediğinizden emin misiniz?',
  hosts_edit: 'Hostları düzenle',
  hosts_title: 'Host Başlığı',
  hosts_type: 'Host Türü',
  hosts_updated: 'Host dosyası güncellendi.',
  hour: 'saat',
  hours: 'saatler',
  http_api_on: 'HTTP API açık',
  http_api_on_desc: '{0} portunda çalışır, Alfred gibi üçüncü parti yazılımlar tarafından hostları değiştirmek için kullanılabilir.',
  http_api_only_local: 'HTTP API sadece 127.0.0.1’i dinler',
  ignore_case: 'Büyük/Küçük Harf Duyarsız',
  import: 'İçe Aktar',
  import_done: 'İçe aktarma tamamlandı.',
  import_fail: 'İçe aktarma başarısız!',
  import_from_url: 'URL’den İçe Aktar',
  is_latest_version_inform: 'Harika, en güncel sürümü kullanıyorsunuz!',
  check_update_failed: 'Güncellemeleri kontrol etme başarısız!',
  update_download_now: 'Güncellemeyi indir',
  update_install_now: 'Yükle ve yeniden başlat',
  update_downloading_desc: '{0} sürümü indiriliyor: {1}',
  update_ready_desc: '{0} sürümü indirildi ve kuruluma hazır.',
  item_found: '{0} öğe bulundu.',
  items: 'öğeler',
  items_found: '{0} öğe bulundu.',
  language: 'Dil',
  last_refresh: 'Son yenileme: ',
  latest_version_desc: 'En son sürüm: {0}',
  line: 'satır',
  lines: 'satırlar',
  loading: 'Yükleniyor...',
  local: 'Yerel',
  match: 'Eşleşme',
  migrate_confirm:
    'SwitchHosts v4.0 yeni bir veri depolama formatı kullanıyor, eski verileri yeni formata taşımak ister misiniz?',
  migrate_data: 'Veri Taşı',
  minimize: 'Küçült',
  minute: 'dakika',
  minutes: 'dakikalar',
  move_items_to_trashcan: 'Çöp kutusuna {0} öğe taşı',
  move_to_trashcan: 'Çöp Kutusuna Taşı',
  multi_chose_folder_switch_all: 'çoklu seçim klasörü, alt öğelerin kontrolünü sağlar',
  need_to_relaunch: 'Yeniden başlatılması gerekiyor',
  need_to_relaunch_after_setting_changed: 'Ayar değiştirildi ve uygulama yeniden başlatıldıktan sonra etkili olacak.',
  never: 'Asla',
  new: 'Yeni',
  new_version_found: 'Yeni sürüm bulundu',
  next: 'Sonraki',
  no_access_to_hosts: 'Hosts dosyasına yazma izni yok.',
  no_record: 'Kayıt yok',
  overwrite: 'Üzerine Yaz',
  password: 'Parola',
  paste: 'Yapıştır',
  port: 'Port',
  preferences: 'Tercihler',
  previous: 'Önceki',
  protocol: 'Protokol',
  proxy: 'Proxy',
  quit: 'Çıkış',
  read_only: 'Salt Okunur',
  redo: 'Yinele',
  refresh: 'Yenile',
  regexp: 'Düzenli İfade',
  reload: 'Yeniden Yükle',
  remote: 'Uzak',
  remove_duplicate_records: 'Yinelenen kayıtları kaldır',
  remove_duplicate_records_desc:
    'Bir alan birden fazla IP\'ye işaret ediyorsa, sadece ilk olanı etkili olacak ve sonrakiler yorum olarak dönüştürülecek.',
  replace: 'Değiştir',
  replace_all: 'Hepsini Değiştir',
  replace_history: 'Geçmişi Değiştir',
  reset: 'Sıfırla',
  reset_data_dir_confirm: 'Veri klasörünü varsayılan adrese ({0}) geri yüklemek istediğinizden emin misiniz?',
  reset_zoom: 'Yakınlaştırmayı Sıfırla',
  search: 'Ara',
  select_all: 'Hepsini Seç',
  selected: 'Seçildi',
  show_dock_icon: 'Dock simgesini göster',
  show_history: 'Geçmişi Göster',
  show_main_window: 'Ana pencereyi göster',
  show_title_on_tray: 'Görev çubuğunda başlığı göster',
  source_code: 'Kaynak Kod',
  success: 'Başarılı!',
  sudo_prompt_title: 'Sudo parolanızı girin',
  system_hosts: 'Sistem Hostları',
  system_hosts_history: 'Sistem Hostlarının geçmiş sürümleri',
  system_hosts_history_delete_confirm: 'Bu öğeyi silmek istediğinizden emin misiniz?',
  system_hosts_history_help: 'Toplam kayıt sayısı bu sınırları aşarsa, en eski kayıt silinecektir.',
  system_hosts_history_limit: 'Maksimum kayıt sayısı: ',
  test: 'Test',
  theme: 'Tema',
  theme_dark: 'Karanlık',
  theme_light: 'Aydınlık',
  title: 'Başlık',
  to_show_source: 'Kaynak kodunu göstermek için çift tıklayın.',
  toggle_developer_tools: 'Geliştirici Araçlarını Aç/Kapat',
  toggle_dock_icon: 'Dock simgesini aç/kapat',
  toggle_full_screen: 'Tam ekranı aç/kapat',
  trashcan: 'Çöp Kutusu',
  trashcan_clear: 'Çöp kutusunu boşalt',
  trashcan_clear_confirm: 'Çöp kutusunu boşaltmak istediğinizden emin misiniz?',
  trashcan_delete_confirm: 'Bu öğeyi tamamen silmek istiyor musunuz?',
  trashcan_restore: 'Geri Yükle',
  tray_mini_window: 'Görev çubuğu simgesi kısayolu',
  undo: 'Geri Al',
  unhide: 'Gizlemeyi Kaldır',
  untitled: 'Başlıksız',
  url_placeholder: 'http:// veya https:// veya file://',
  usage_data_agree: 'Evet, anonimleştirilmiş kullanım verilerini gönder',
  usage_data_help:
    'Anonim kullanım verilerini periyodik olarak göndererek SwitchHosts\'u iyileştirmemize yardımcı olmak ister misiniz?',
  usage_data_title: 'SwitchHosts\'u Daha İyi Yapın!',
  use_proxy: 'Proxy Kullan',
  use_system_window_frame: 'Sistem pencere çerçevesini kullanın, uygulamanın yeniden başlatılması gereklidir',
  view: 'Görüntüle',
  where_is_my_data: 'Verilerim nerede saklanıyor?',
  where_is_my_hosts: 'Hosts dosyam nerede?',
  window: 'Pencere',
  write_mode: 'Yazma modu',
  write_mode_append_help: 'Yeni kayıtları sistem hosts dosyasının sonuna ekleyin.',
  write_mode_overwrite_help: 'Yeni kayıtlarla sistem hosts dosyasını üzerine yazın.',
  write_mode_set: 'Yazma modunu ayarla',
  your_data_is: 'Veri dosyalarınız şurada saklanıyor:',
  your_hosts_file_is: 'Hosts dosyanız şu konumda bulunuyor:',
  zoom: 'Yakınlaştır',
  zoom_in: 'Yakınlaştır',
  zoom_out: 'Uzaklaştır',
}


================================================
FILE: src/common/i18n/languages/zh-hant.ts
================================================
/**
 * @author: rayatn1011
 * @homepage: https://github.com/rayatn1011
 */

import { LanguageDict } from '@common/types'

const lang: LanguageDict = {
  _app_name: 'SwitchHosts',
  _key: 'zh-hant',
  _name: '中文',
  about: '關於',
  acknowledgement: '特別感謝',
  advanced: '進階',
  all: '全部',
  append: '附加',
  auto_refresh: '自動更新',
  btn_cancel: '取消',
  btn_ok: '確定',
  change: '修改',
  check_update: '檢查更新',
  choice_mode: '選擇模式',
  choice_mode_default: '預設',
  choice_mode_desc: '只對頂層項目有效,每個資料夾可設定自己的選擇模式。',
  choice_mode_multiple: '多選',
  choice_mode_single: '單選',
  choices: '選項',
  chosen: '已選',
  clear_history: '清除歷史紀錄',
  click_to_open: '點擊開啟',
  close: '關閉',
  colon: ':',
  commands: '指令',
  commands_help: '每次 Hosts 應用後將執行下面的系統指令:',
  commands_title: 'Hosts 應用後指令',
  comment_current_line: '註解當前行',
  content: '內容',
  copy: '複製',
  cut: '剪下',
  day: '天',
  days: '天',
  delete: '刪除',
  download: '下載',
  edit: '編輯',
  export: '匯出',
  export_done: '匯出已完成。',
  fail: '操作失敗!',
  feedback: '意見回饋',
  file: '檔案',
  find: '尋找',
  find_all: '尋找所有',
  find_and_replace: '尋找並替換',
  find_history: '尋找歷史',
  folder: '資料夾',
  front: '前置',
  general: '一般',
  group: '群組',
  help: 'Help',
  hide: '隱藏',
  hide_at_launch: '啟動時隱藏',
  hide_dock_icon: '隱藏工作列圖示',
  hide_history: '隱藏歷史紀錄',
  hide_others: '隱藏其他',
  homepage: '首頁',
  host: '主機',
  hosts_add: '新增 hosts',
  hosts_delete: '刪除當前方案',
  hosts_delete_confirm: '確定要刪除當前方案嗎?',
  hosts_edit: '編輯 hosts',
  hosts_title: 'Hosts 標題',
  hosts_type: 'Hosts 類型',
  hosts_updated: 'Hosts 檔案已更新。',
  hour: '小時',
  hours: '小時',
  http_api_on: '開啟 HTTP API',
  http_api_on_desc: '運行於 {0} 通訊埠,可用於 Alfred 等第三方應用切換 hosts。',
  http_api_only_local: 'HTTP API 僅監聽 127.0.0.1',
  ignore_case: '忽略大小寫',
  import: '匯入',
  import_done: '匯入已完成。',
  import_fail: '匯入失敗!',
  import_from_url: '從 URL 匯入',
  is_latest_version_inform: '太棒了,你正在執行的是最新版本!',
  check_update_failed: '檢查更新失敗!',
  update_download_now: '下載更新',
  update_install_now: '安裝並重新啟動',
  update_downloading_desc: '正在下載版本 {0}:{1}',
  update_ready_desc: '版本 {0} 已下載完成,可以開始安裝。',
  item_found: '{0} 項符合',
  items: '項',
  items_found: '{0} 項符合',
  language: '語言',
  last_refresh: '最後更新:',
  latest_version_desc: '最新的版本為:{0}',
  line: '行',
  lines: '行',
  loading: '載入中...',
  local: '本地',
  match: '符合',
  migrate_confirm: 'SwitchHosts v4.0 使用了新的資料儲存格式,是否遷移舊資料到新格式?',
  migrate_data: '遷移資料',
  minimize: '最小化',
  minute: '分鐘',
  minutes: '分鐘',
  move_items_to_trashcan: '移動 {0} 項到垃圾桶',
  move_to_trashcan: '移到垃圾桶',
  multi_chose_folder_switch_all: '多選資料夾開關控制子項目',
  need_to_relaunch: '需要重啟',
  need_to_relaunch_after_setting_changed: '設定已更改,應用重啟後生效。',
  never: '永不',
  new: '新建',
  new_version_found: '發現新版本',
  next: '下一個',
  no_access_to_hosts: '沒有寫入 Hosts 檔案的權限。',
  no_record: '沒有紀錄',
  overwrite: '覆寫',
  password: '密碼',
  paste: '貼上',
  port: '通訊埠',
  preferences: '選項',
  previous: '上一個',
  protocol: '協議',
  proxy: '代理',
  quit: '退出',
  read_only: '唯讀',
  redo: '重做',
  refresh: '更新',
  regexp: '正規表達式',
  reload: '重載',
  remote: '遠端',
  remove_duplicate_records: '移除重複的紀錄',
  remove_duplicate_records_desc: '如果一個網域指向多個 IP,只有第一條會生效,剩下的將被轉為註解。',
  replace: '替換',
  replace_all: '替換全部',
  replace_history: '替換歷史',
  reset: '重設',
  reset_data_dir_confirm: '確定要把資料夾重設為預設路徑嗎?({0})?',
  reset_zoom: '重設縮放',
  search: '搜尋',
  select_all: '全選',
  selected: '已選',
  show_dock_icon: '顯示工作列圖示',
  show_history: '顯示歷史紀錄',
  show_main_window: '顯示主視窗',
  show_title_on_tray: '在通知區域顯示標題',
  source_code: '原始碼',
  success: '操作成功!',
  sudo_prompt_title: '請輸入你的登入密碼(sudo 密碼)',
  system_hosts: '系統 Hosts',
  system_hosts_history: '系統 Hosts 歷史版本',
  system_hosts_history_delete_confirm: '確定要刪除該項紀錄嗎?',
  system_hosts_history_help: '如果歷史紀錄的總數超過這個限制,最舊的紀錄將被刪除。',
  system_hosts_history_limit: '最大紀錄數:',
  test: '測試',
  theme: '主題',
  theme_dark: '深色',
  theme_light: '亮色',
  title: '標題',
  to_show_source: '雙擊顯示原始碼。',
  toggle_developer_tools: '切換開發者工具',
  toggle_dock_icon: '顯示/隱藏任務列圖示',
  toggle_full_screen: '切換全螢幕',
  trashcan: '垃圾桶',
  trashcan_clear: '清除垃圾桶',
  trashcan_clear_confirm: '確定要清除垃圾桶嗎?',
  trashcan_delete_confirm: '要完全清除本項嗎?',
  trashcan_restore: '復原',
  tray_mini_window: '任務列快捷視窗',
  undo: '取消',
  unhide: '取消隱藏',
  untitled: '未命名',
  url_placeholder: 'http:// 或 https:// 或 file://',
  usage_data_agree: '好的,寄送匿名的使用資料',
  usage_data_help:
    '您願意寄送匿名的使用資料來幫助我們改善 SwitchHosts 嗎?資料中不會包含任何隱私資訊。',
  usage_data_title: '幫助改善 SwitchHosts',
  use_proxy: '使用代理',
  use_system_window_frame: '使用系統視窗外框,需要重啟程式',
  view: '視窗',
  where_is_my_data: '我的資料儲存在哪裡?',
  where_is_my_hosts: '我的 hosts 檔案在哪裡?',
  window: 'Window',
  write_mode: '寫入模式',
  write_mode_append_help: '新紀錄將附加到現有系統 hosts 檔案後面。',
  write_mode_overwrite_help: '新紀錄將覆寫現有系統 hosts 檔案。',
  write_mode_set: '設定寫入模式',
  your_data_is: '你的資料在:',
  your_hosts_file_is: '你的 hosts 檔案在:',
  zoom: '縮放',
  zoom_in: '放大',
  zoom_out: '縮小',
}

export default lang


================================================
FILE: src/common/i18n/languages/zh.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { LanguageDict } from '@common/types'

const lang: LanguageDict = {
  _app_name: 'SwitchHosts',
  _key: 'zh',
  _name: '中文',
  about: '关于',
  acknowledgement: '特别致谢',
  advanced: '高级',
  all: '全部',
  append: '追加',
  auto_refresh: '自动刷新',
  btn_cancel: '取消',
  btn_ok: '确定',
  change: '更改',
  check_update: '检查更新',
  choice_mode: '选择模式',
  choice_mode_default: '默认',
  choice_mode_desc: '只对顶层项目生效,每个文件夹可设置自己的选择模式。',
  choice_mode_multiple: '多选',
  choice_mode_single: '单选',
  choices: '选项',
  chosen: '已选',
  clear_history: '清除历史记录',
  click_to_open: '点击打开',
  close: '关闭',
  colon: ':',
  commands: '命令',
  commands_help: '每次 Hosts 应用后将执行下面的系统命令:',
  commands_title: 'Hosts 应用后命令',
  comment_current_line: '注释当前行',
  content: '内容',
  copy: '复制',
  cut: '剪切',
  day: '天',
  days: '天',
  delete: '删除',
  download: '下载',
  edit: '编辑',
  export: '导出',
  export_done: '导出已完成。',
  fail: '操作失败!',
  feedback: '意见反馈',
  file: '文件',
  find: '查找',
  find_all: '查找所有',
  find_and_replace: '查找并替换',
  find_history: '查找历史',
  folder: '文件夹',
  front: '前置',
  general: '通用',
  group: '组合',
  help: 'Help',
  hide: '隐藏',
  hide_at_launch: '启动时隐藏',
  hide_dock_icon: '隐藏任务栏图标',
  hide_history: '隐藏历史记录',
  hide_others: '隐藏其他',
  homepage: '主页',
  host: '主机',
  hosts_add: '添加 hosts',
  hosts_delete: '删除当前方案',
  hosts_delete_confirm: '确实要删除当前方案吗?',
  hosts_edit: '编辑 hosts',
  hosts_title: 'Hosts 标题',
  hosts_type: 'Hosts 类型',
  hosts_updated: 'Hosts 文件已更新。',
  hour: '小时',
  hours: '小时',
  http_api_on: '开启 HTTP API',
  http_api_on_desc: '运行于 {0} 端口,可用于 Alfred 等第三方软件切换 hosts。',
  http_api_only_local: 'HTTP API 仅监听 127.0.0.1',
  ignore_case: '忽略大小写',
  import: '导入',
  import_done: '导入已完成。',
  import_fail: '导入失败!',
  import_from_url: '从 URL 导入',
  is_latest_version_inform: '太棒了,你正在运行的是最新版本!',
  check_update_failed: '检查更新失败!',
  update_download_now: '下载更新',
  update_install_now: '安装并重启',
  update_downloading_desc: '正在下载版本 {0}:{1}',
  update_ready_desc: '版本 {0} 已下载完成,可以开始安装。',
  item_found: '{0} 项匹配',
  items: '项',
  items_found: '{0} 项匹配',
  language: '语言',
  last_refresh: '最后刷新:',
  latest_version_desc: '最新的版本为:{0}',
  line: '行',
  lines: '行',
  loading: '加载中...',
  local: '本地',
  match: '匹配',
  migrate_confirm: 'SwitchHosts v4.0 使用了新的数据存储格式,是否迁移旧数据为新格式?',
  migrate_data: '迁移数据',
  minimize: '最小化',
  minute: '分钟',
  minutes: '分钟',
  move_items_to_trashcan: '移动 {0} 项到回收站',
  move_to_trashcan: '移到回收站',
  multi_chose_folder_switch_all: '多选文件夹开关控制子项目',
  need_to_relaunch: '需要重启',
  need_to_relaunch_after_setting_changed: '设置已更改,应用重启后生效。',
  never: '从不',
  new: '新建',
  new_version_found: '发现新版本',
  next: '下一个',
  no_access_to_hosts: '没有写入 Hosts 文件的权限。',
  no_record: '没有记录',
  overwrite: '覆盖',
  password: '密码',
  paste: '粘贴',
  port: '端口',
  preferences: '选项',
  previous: '上一个',
  protocol: '协议',
  proxy: '代理',
  quit: '退出',
  read_only: '只读',
  redo: '重做',
  refresh: '刷新',
  regexp: '正则表达式',
  reload: '重载',
  remote: '远程',
  remove_duplicate_records: '移除重复的记录',
  remove_duplicate_records_desc: '如果一个域名指向多个 IP,只有第一条会生效,后面的将被转为注释。',
  replace: '替换',
  replace_all: '替换所有',
  replace_history: '替换历史',
  reset: '重置',
  reset_data_dir_confirm: '确定要把数据文件夹重置为默认地址吗?({0})?',
  reset_zoom: '重置缩放',
  search: '搜索',
  select_all: '全选',
  selected: '已选',
  show_dock_icon: '显示任务栏图标',
  show_history: '显示历史记录',
  show_main_window: '显示主窗口',
  show_title_on_tray: '在系统托盘显示标题',
  source_code: '源码',
  success: '操作成功!',
  sudo_prompt_title: '请输入你的登录密码(sudo 密码)',
  system_hosts: '系统 Hosts',
  system_hosts_history: '系统 Hosts 历史版本',
  system_hosts_history_delete_confirm: '确实要删除该项记录吗?',
  system_hosts_history_help: '如果历史记录的总数超过这个限制,最老的记录将被删除。',
  system_hosts_history_limit: '最大记录数:',
  test: '测试',
  theme: '主题',
  theme_dark: '夜间',
  theme_light: '明亮',
  title: '标题',
  to_show_source: '双击显示源代码。',
  toggle_developer_tools: '切换开发者工具',
  toggle_dock_icon: '显示/隐藏任务栏图标',
  toggle_full_screen: '切换全屏',
  trashcan: '回收站',
  trashcan_clear: '清空回收站',
  trashcan_clear_confirm: '确实要清空回收站吗?',
  trashcan_delete_confirm: '要彻底删除本项吗?',
  trashcan_restore: '还原',
  tray_mini_window: '任务栏快捷小窗',
  undo: '撤销',
  unhide: '取消隐藏',
  untitled: '未命名',
  url_placeholder: 'http:// 或 https:// 或 file://',
  usage_data_agree: '好的,发送匿名的使用数据',
  usage_data_help:
    '您愿意发送匿名的使用数据来帮助我们改进 SwitchHosts 吗?数据中不会包含任何隐私信息。',
  usage_data_title: '帮助改进 SwitchHosts',
  use_proxy: '使用代理',
  use_system_window_frame: '使用系统窗口框架,需要重启程序',
  view: '视图',
  where_is_my_data: '我的数据存储在哪里?',
  where_is_my_hosts: '我的 hosts 文件在哪里?',
  window: 'Window',
  write_mode: '写入模式',
  write_mode_append_help: '新记录将追加到现有系统 hosts 文件末尾。',
  write_mode_overwrite_help: '新记录将覆盖现有系统 hosts 文件。',
  write_mode_set: '设置写入模式',
  your_data_is: '你的数据在:',
  your_hosts_file_is: '你的 hosts 文件在:',
  zoom: '缩放',
  zoom_in: '放大',
  zoom_out: '缩小',
}

export default lang


================================================
FILE: src/common/newlines.ts
================================================
export type LineEnding = '\n' | '\r\n'

const LINE_ENDING_RE = /\r\n?/g

export function normalizeLineEndings(content: string): string {
  return content.replace(LINE_ENDING_RE, '\n')
}

export function getLineEndingForPlatform(platform = process.platform): LineEnding {
  if (platform === 'win32') {
    return '\r\n'
  }

  return '\n'
}

export function restoreLineEndings(content: string, lineEnding: LineEnding): string {
  const normalized = normalizeLineEndings(content)

  if (lineEnding === '\r\n') {
    return normalized.replace(/\n/g, '\r\n')
  }

  return normalized
}


================================================
FILE: src/common/normalize.ts
================================================
/**
 * normalize
 * @author: oldj
 * @homepage: https://oldj.net
 */

import * as os from 'os'

const default_options = {
  remove_duplicate_records: false,
}

export type INormalizeOptions = Partial<typeof default_options>

interface IHostsLineObj {
  ip: string
  domains: string[]
  comment: string
}

interface IDomainsIPMap {
  [domain: string]: string
}

export const parseLine = (line: string): IHostsLineObj => {
  let [cnt, ...cmt] = line.split('#')
  let comment = cmt.join('#').trim()

  let [ip, ...domains] = cnt.trim().replace(/\s+/g, ' ').split(' ')

  return { ip, domains, comment }
}

export const formatLine = (o: Partial<IHostsLineObj>): string => {
  let comment = o.comment || ''
  if (comment) {
    comment = '# ' + comment
  }
  return [o.ip || '', (o.domains || []).join(' '), comment].join(' ').trim()
}

const removeDuplicateRecords = (content: string): string => {
  let domain_ip_map: IDomainsIPMap = {}
  let lines = content.split('\n')
  let new_lines: string[] = []

  lines.map((line) => {
    let { ip, domains, comment } = parseLine(line)

    if (!ip || domains.length === 0) {
      new_lines.push(line)
      return
    }

    const ipv = /:/.test(ip) ? 6 : 4

    let new_domains: string[] = []
    let duplicate_domains: string[] = []
    domains.map((domain) => {
      const domain_v = `${domain}_${ipv}`
      if (domain_v in domain_ip_map) {
        duplicate_domains.push(domain)
      } else {
        new_domains.push(domain)
        domain_ip_map[domain_v] = ip
      }
    })

    if (new_domains.length > 0) {
      new_lines.push(formatLine({ ip, domains: new_domains, comment }))
    }
    if (duplicate_domains.length > 0) {
      new_lines.push(
        formatLine({
          comment:
            'invalid hosts (repeated): ' +
            formatLine({ ip, domains: duplicate_domains }),
        }),
      )
    }
  })

  return new_lines.join(os.EOL)
}

export default (
  hosts_content: string,
  options: INormalizeOptions = {},
): string => {
  // 在这儿执行去重等等操作
  if (options.remove_duplicate_records) {
    hosts_content = removeDuplicateRecords(hosts_content)
  }

  return hosts_content
}


================================================
FILE: src/common/tree.ts
================================================
export type NodeIdType = string

export interface ITreeNodeData {
  id: NodeIdType
  title?: string
  can_select?: boolean // 是否可以被选中,默认为 true
  can_drag?: boolean // 是否可以拖动,默认为 true
  can_drop_before?: boolean // 是否可以接受 drop before,默认为 true
  can_drop_in?: boolean // 是否可以接受 drop in,默认为 true
  can_drop_after?: boolean // 是否可以接受 drop after,默认为 true
  is_collapsed?: boolean
  children?: ITreeNodeData[]

  [key: string]: any
}

interface IWithChildren {
  children?: IWithChildren[]
}

export function flatten<T extends IWithChildren>(tree_list: T[]): T[] {
  let arr: any[] = []

  Array.isArray(tree_list) &&
    tree_list.map((item) => {
      if (!item) return

      arr.push(item)

      if (Array.isArray(item.children)) {
        let a2 = flatten(item.children)
        arr = arr.concat(a2)
      }
    })

  return arr
}

interface IWidthId extends IWithChildren {
  id: string
}

export function getNodeById<T extends IWidthId>(
  tree_list: T[],
  id: string,
): T | undefined {
  return flatten(tree_list).find((i) => i.id === id)
}


================================================
FILE: src/common/types.d.ts
================================================
/**
 * types
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { HostsType } from '@common/data'
import { MenuItemConstructorOptions, WebContents } from 'electron'
import { default as lang } from './i18n/languages/en'
import * as actions from '@main/actions'

export type LanguageDict = typeof lang
export type LanguageKey = keyof LanguageDict

export interface IActionFunc {
  sender: WebContents
}

export type Actions = typeof actions & IActionFunc

export interface IMenuItemOption extends MenuItemConstructorOptions {
  // 参见:https://www.electronjs.org/docs/api/menu-item

  _click_evt?: string
}

export interface IPopupMenuOption {
  menu_id: string
  items: IMenuItemOption[]
}

export interface IFindPosition {
  start: number
  end: number
  line: number
  line_pos: number
  end_line: number
  end_line_pos: number
  before: string
  match: string
  after: string
}

export interface IFindSplitter {
  before: string
  match: string
  after: string
  replace?: string
}

export interface IFindItem {
  item_id: string
  item_title: string
  item_type: HostsType
  positions: IFindPosition[]
  splitters: IFindSplitter[]
}

export type IFindShowSourceParam = IFindPosition & {
  item_id: string
  [key: string]: any
}


================================================
FILE: src/common/update.ts
================================================
export interface AppUpdateInfo {
  version: string
  releaseName?: string | null
  releaseNotes?: string | null
}

export interface AppUpdateProgress {
  percent: number
  transferred: number
  total: number
  bytesPerSecond: number
}

export interface AppDownloadedUpdateInfo extends AppUpdateInfo {
  downloadedFile?: string | null
}


================================================
FILE: src/common/utils/wait.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

export default (ms: number) => new Promise((resolve) => setTimeout(resolve, ms))


================================================
FILE: src/main/actions/checkUpdate.ts
================================================
import * as updater from '@main/core/updater'

export default async (): Promise<boolean | null> => {
  try {
    const update = await updater.checkUpdate()
    return !!update
  } catch (error) {
    console.error(error)
    return null
  }
}


================================================
FILE: src/main/actions/closeMainWindow.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

export default async () => {
  let win = global.main_win
  win && win.isClosable() && win.close()
}


================================================
FILE: src/main/actions/cmd/changeDataDir.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { app, BrowserWindow, dialog, OpenDialogOptions, OpenDialogReturnValue } from 'electron'
import { localdb } from '@main/data'
import getDataFolder, { getDefaultDataDir } from '@main/libs/getDataDir'
import getI18N from '@main/core/getI18N'
import { IActionFunc } from '@common/types'

export default async function (
  this: IActionFunc,
  to_default?: boolean,
): Promise<string | undefined> {
  let { sender } = this
  let { lang } = await getI18N()
  let current_dir = getDataFolder()
  let dir: string = ''

  if (to_default) {
    dir = getDefaultDataDir()
  } else {
    let parent = BrowserWindow.fromWebContents(sender)
    if (parent?.isFullScreen()) {
      parent?.setFullScreen(false)
    }

    let options: OpenDialogOptions = {
      // title: '选择数据目录',
      defaultPath: current_dir,
      properties: ['openDirectory', 'createDirectory'],
    }

    let r: OpenDialogReturnValue

    if (parent) {
      r = await dialog.showOpenDialog(parent, options)
    } else {
      r = await dialog.showOpenDialog(options)
    }

    if (r.canceled) {
      return
    }

    dir = r.filePaths[0]
  }

  if (!dir || dir === current_dir) {
    return
  }

  await localdb.dict.local.set('data_dir', dir)
  dialog.showMessageBoxSync({
    message: lang.need_to_relaunch_after_setting_changed,
  })
  app.relaunch()
  app.exit(0)

  return dir
}


================================================
FILE: src/main/actions/cmd/clearHistory.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { cfgdb } from '@main/data'

export default async () => {
  return await cfgdb.collection.cmd_history.remove()
}


================================================
FILE: src/main/actions/cmd/deleteHistory.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { cfgdb } from '@main/data'

export default async (_id: string) => {
  return await cfgdb.collection.cmd_history.delete((i) => i._id === _id)
}


================================================
FILE: src/main/actions/cmd/focusMainWindow.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

export default () => {
  global.main_win.show()
  global.main_win.focus()
}


================================================
FILE: src/main/actions/cmd/getHistoryList.ts
================================================
/**
 * getHistoryList
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { cfgdb } from '@main/data'
import { ICommandRunResult } from '@common/data'

export default async (): Promise<ICommandRunResult[]> => {
  return await cfgdb.collection.cmd_history.all()
}


================================================
FILE: src/main/actions/cmd/toggleDevTools.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

export default () => {
  let win = global.main_win
  if (!win) return

  win.webContents.toggleDevTools()
}


================================================
FILE: src/main/actions/cmd/tryToRun.ts
================================================
/**
 * run
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { cfgdb } from '@main/data'
import { ICommandRunResult } from '@common/data'
import { exec } from 'child_process'
import { broadcast } from '@main/core/agent'
import events from '@common/events'

const run = (cmd: string): Promise<ICommandRunResult> =>
  new Promise((resolve) => {
    exec(cmd, (error, stdout, stderr) => {
      // command output is in stdout
      let success: boolean = !error

      resolve({
        success,
        stdout,
        stderr,
        add_time_ms: new Date().getTime(),
      })
    })
  })

export default async () => {
  let cmd = await cfgdb.dict.cfg.get('cmd_after_hosts_apply')

  if (!cmd || typeof cmd !== 'string' || !cmd.trim()) {
    return
  }

  console.log(`to run cmd...`)
  let result = await run(cmd)
  console.log(result)
  await cfgdb.collection.cmd_history.insert(result)
  broadcast(events.cmd_run_result, result)

  // auto delete old records
  const max_records = 200
  let all = await cfgdb.collection.cmd_history.all<ICommandRunResult>()
  if (all.length > max_records) {
    let n = all.length - max_records
    for (let i = 0; i < n; i++) {
      await cfgdb.collection.cmd_history.delete((item) => item._id === all[i]._id)
    }
  }

  global.tracer.add(`cmd:${result.success ? 1 : 0}`)
}


================================================
FILE: src/main/actions/config/all.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { cfgdb } from '@main/data'
import default_configs, { ConfigsType } from '@common/default_configs'

export default async (): Promise<ConfigsType> => {
  if (!default_configs.locale && global.system_locale) {
    default_configs.locale = global.system_locale
  }

  let cfgs: Partial<ConfigsType> = await cfgdb.dict.cfg.all()
  return Object.assign({}, default_configs, cfgs)
}


================================================
FILE: src/main/actions/config/get.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { cfgdb } from '@main/data'
import default_configs, { ConfigsType } from '@common/default_configs'

export default async <K extends keyof ConfigsType>(key: K) => {
  return (await cfgdb.dict.cfg.get(key, default_configs[key])) as ConfigsType[K]
}


================================================
FILE: src/main/actions/config/set.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { cfgdb } from '@main/data'
import { ConfigsType } from '@common/default_configs'

export default async <K extends keyof ConfigsType>(key: K, value: ConfigsType[K]) => {
  console.log(`config:store.set [${key}]: ${value}`)
  await cfgdb.dict.cfg.set(key, value)
}


================================================
FILE: src/main/actions/config/update.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { updateTrayTitle } from '@main/actions'
import { cfgdb } from '@main/data'
import * as http_api from '@main/http'
import { makeMainMenu } from '@main/ui/menu'
import { ConfigsType } from '@common/default_configs'
import { app } from 'electron'

export default async (data: Partial<ConfigsType>) => {
  const old_configs = (await cfgdb.dict.cfg.all()) as ConfigsType

  await cfgdb.dict.cfg.update(data)

  await updateTrayTitle(!!data.show_title_on_tray)
  if (old_configs.locale !== data.locale) {
    makeMainMenu(data.locale)
  }

  if (old_configs.http_api_on !== data.http_api_on) {
    if (data.http_api_on) {
      http_api.start(<boolean>data.http_api_only_local)
    } else {
      http_api.stop()
    }
  } else if (old_configs.http_api_only_local !== data.http_api_only_local) {
    if (data.http_api_on) {
      await http_api.stop()
      http_api.start(<boolean>data.http_api_only_local)
    }
  }

  if (old_configs.hide_dock_icon !== data.hide_dock_icon) {
    if (!app.dock) {
      return
    }

    if (data.hide_dock_icon) {
      app.dock.hide()
    } else {
      app.dock.show().catch((e) => console.error(e))
    }
  }
}


================================================
FILE: src/main/actions/downloadUpdate.ts
================================================
import * as updater from '@main/core/updater'

export default async (): Promise<boolean> => {
  await updater.downloadUpdate()
  return true
}


================================================
FILE: src/main/actions/find/addHistory.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import getHistory from '@main/actions/find/getHistory'
import setHistory, { IFindHistoryData } from '@main/actions/find/setHistory'

const MAX_LENGTH = 20

export default async (data: IFindHistoryData) => {
  let history_all = await getHistory()

  // remove old
  history_all = history_all.filter((i) => i.value !== data.value)

  // insert new
  history_all.push(data)

  while (history_all.length > MAX_LENGTH) {
    history_all.shift()
  }

  await setHistory(history_all)

  return history_all
}


================================================
FILE: src/main/actions/find/addReplaceHistory.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import getReplaceHistory from '@main/actions/find/getReplaceHistory'
import setReplaceHistory from '@main/actions/find/setReplaceHistory'

const MAX_LENGTH = 20

export default async (value: string) => {
  let history_all = await getReplaceHistory()

  // remove old
  history_all = history_all.filter((v) => v !== value)

  // insert new
  history_all.push(value)

  while (history_all.length > MAX_LENGTH) {
    history_all.shift()
  }

  await setReplaceHistory(history_all)

  return history_all
}


================================================
FILE: src/main/actions/find/findBy.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import splitContent from '@main/actions/find/splitContent'
import getContentOfHosts from '@main/actions/hosts/getContent'
import { flatten } from '@common/hostsFn'
import { IFindItem } from '@common/types'
import findInContent from 'src/main/actions/find/findPositionsInContent'
import { getList } from '../index'

export interface IFindOptions {
  is_regexp: boolean
  is_ignore_case: boolean
}

export default async (keyword: string, options: IFindOptions): Promise<IFindItem[]> => {
  console.log(keyword)
  let result_items: IFindItem[] = []

  let tree = await getList()
  let items = flatten(tree)

  let exp: RegExp
  if (options.is_regexp) {
    exp = new RegExp(keyword, options.is_ignore_case ? 'ig' : 'g')
  } else {
    let kw = keyword.replace(/([.^$([?*+])/gi, '\\$1')
    exp = new RegExp(kw, options.is_ignore_case ? 'ig' : 'g')
  }

  for (let item of items) {
    const item_type = item.type || 'local'
    if (item_type === 'group' || item_type === 'folder') {
      continue
    }
    let content = await getContentOfHosts(item.id)
    let positions = findInContent(content, exp)
    if (positions.length === 0) {
      continue
    }

    result_items.push({
      item_title: item.title || '',
      item_id: item.id,
      item_type,
      positions,
      splitters: splitContent(content, positions),
    })
  }

  return result_items
}


================================================
FILE: src/main/actions/find/findPositionsInContent.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { IFindPosition } from '@common/types'

type MatchResult = Pick<
  IFindPosition,
  'start' | 'end' | 'before' | 'match' | 'after' | 'line' | 'line_pos' | 'end_line' | 'end_line_pos'
>

export default (content: string, exp: RegExp): MatchResult[] => {
  let result_items: MatchResult[] = []

  let m = content.match(exp)
  if (!m) {
    return []
  }

  let line = 1
  let start = 0

  let cnt = content
  for (let i of m) {
    let idx = cnt.indexOf(i)
    if (idx === -1) continue

    let head = cnt.slice(0, idx)
    cnt = cnt.slice(idx + i.length)

    let head_lines = head.split('\n')
    line += head_lines.length - 1
    start += head.length
    let before_lines = content.slice(0, start).split('\n')
    let before = before_lines[before_lines.length - 1]
    let after = cnt.split('\n')[0]

    let i_ln = i.split('\n')
    let end_line = line + i_ln.length - 1
    let end_line_pos = before.length + i.length
    if (i_ln.length > 1) {
      end_line_pos = i_ln[i_ln.length - 1].length
    }

    result_items.push({
      start,
      end: start + i.length,
      before,
      match: i,
      after,
      line,
      line_pos: before.length,
      end_line,
      end_line_pos,
    })

    start += i.length
  }

  return result_items
}


================================================
FILE: src/main/actions/find/getHistory.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { IFindHistoryData } from '@main/actions/find/setHistory'
import { cfgdb } from '@main/data'

export default async (): Promise<IFindHistoryData[]> => {
  return (await cfgdb.list.find_history.all()) as IFindHistoryData[]
}


================================================
FILE: src/main/actions/find/getReplaceHistory.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { cfgdb } from '@main/data'

export default async (): Promise<string[]> => {
  return (await cfgdb.list.replace_history.all()) as string[]
}


================================================
FILE: src/main/actions/find/setHistory.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { cfgdb } from '@main/data'

export interface IFindHistoryData {
  value: string
  is_regexp: boolean
  is_ignore_case: boolean
}

export default async (data: IFindHistoryData[]) => {
  await cfgdb.list.find_history.set(data)
}


================================================
FILE: src/main/actions/find/setReplaceHistory.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { cfgdb } from '@main/data'

export default async (data: string[]) => {
  await cfgdb.list.replace_history.set(data)
}


================================================
FILE: src/main/actions/find/show.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { makeWindow } from '@main/ui/find'

export default async () => {
  if (!global.find_win) {
    global.find_win = await makeWindow()
  }

  global.find_win?.show()
  global.find_win?.focus()
}


================================================
FILE: src/main/actions/find/splitContent.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { IFindPosition, IFindSplitter } from '@common/types'

type MatchResult = Pick<IFindPosition, 'start' | 'end' | 'match'> & {
  [key: string]: any
}

export default (content: string, find_results: MatchResult[]): IFindSplitter[] => {
  let spliters: IFindSplitter[] = []

  let last_end = 0
  find_results.map((r, idx) => {
    let { start, match } = r
    let before = content.slice(last_end, start)
    let after = ''

    last_end += before.length + match.length
    if (idx === find_results.length - 1) {
      after = content.slice(last_end)
    }

    let spliter: IFindSplitter = {
      before,
      after,
      match,
    }

    spliters.push(spliter)
  })

  return spliters
}


================================================
FILE: src/main/actions/getBasicData.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { swhdb } from '@main/data'
import { IHostsBasicData, IHostsListObject, ITrashcanListObject, VersionType } from '@common/data'
import { flatten } from '@common/hostsFn'
import { v4 as uuid4 } from 'uuid'
import version from '@/version.json'

const app_version = version as unknown as VersionType

const normalizeList = (list: IHostsListObject[]): IHostsListObject[] => {
  let flat = flatten(list)
  flat.map((item) => {
    if (!item.id) {
      item.id = uuid4()
    }
  })

  return list
}

const normalizeTrashcan = (list: ITrashcanListObject[]): ITrashcanListObject[] => {
  list.map((item) => {
    if (!item.id) {
      item.id = uuid4()
    }
  })

  return list
}

export default async (): Promise<IHostsBasicData> => {
  const default_data: IHostsBasicData = {
    list: [],
    trashcan: [],
    version: app_version,
  }

  let list = normalizeList(await swhdb.list.tree.all())
  let trashcan = normalizeTrashcan(await swhdb.list.trashcan.all())
  let v = (await swhdb.dict.meta.get<VersionType>('version', app_version)) || [0, 0, 0, 0]

  return {
    ...default_data,
    list,
    trashcan,
    version: v,
  }
}


================================================
FILE: src/main/actions/getDataDir.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import getDataDir from '@main/libs/getDataDir'

export default async () => getDataDir()


================================================
FILE: src/main/actions/getDefaultDataDir.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { getDefaultDataDir } from '@main/libs/getDataDir'

export default async () => getDefaultDataDir()


================================================
FILE: src/main/actions/hosts/deleteHistory.ts
================================================
/**
 * removeHistory
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { swhdb } from '@main/data'

export default async (id: string) => {
  console.log('delete history #' + id)
  await swhdb.collection.history.delete((item) => item.id === id)
}


================================================
FILE: src/main/actions/hosts/getContent.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { configGet, getItemFromList, getList } from '@main/actions'
import { swhdb } from '@main/data'
import { IHostsContentObject } from '@common/data'
import { findItemById, flatten } from '@common/hostsFn'
import { normalizeLineEndings } from '@common/newlines'

const getContentById = async (id: string) => {
  let hosts_content = await swhdb.collection.hosts.find<IHostsContentObject>((i) => i.id === id)
  return normalizeLineEndings(hosts_content?.content || '')
}

const getContentOfHosts = async (id: string): Promise<string> => {
  let hosts = await getItemFromList(id)
  if (!hosts) {
    return await getContentById(id)
  }

  const { type } = hosts
  if (!type || type === 'local' || type === 'remote') {
    return await getContentById(id)
  }

  let list = await getList()

  let multi_chose_folder_switch_all = await configGet('multi_chose_folder_switch_all')
  let isSkipFolder = multi_chose_folder_switch_all && hosts.folder_mode !== 1

  if (type === 'folder' && !isSkipFolder) {
    const items = flatten(hosts.children || [])

    let a = await Promise.all(
      items.map(async (item) => {
        return `# file: ${item.title}\n` + (await getContentOfHosts(item.id))
      }),
    )
    return a.join('\n\n')
  }

  if (type === 'group') {
    let a = await Promise.all(
      (hosts.include || []).map(async (id) => {
        let item = findItemById(list, id)
        if (!item) return ''

        return `# file: ${item.title}\n` + (await getContentOfHosts(id))
      }),
    )
    return a.join('\n\n')
  }

  return ''
}

export default getContentOfHosts


================================================
FILE: src/main/actions/hosts/getHistoryList.ts
================================================
/**
 * getHistoryList
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { swhdb } from '@main/data'
import { IHostsHistoryObject } from '@common/data'

export default async (): Promise<IHostsHistoryObject[]> => {
  let list = await swhdb.collection.history.all<IHostsHistoryObject>()

  list = list.map((item) => {
    item.content = item.content || ''
    return item
  })

  return list
}


================================================
FILE: src/main/actions/hosts/getPathOfSystemHostsPath.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

export default async (): Promise<string> => {
  // Windows 系统有可能不安装在 C 盘
  return process.platform === 'win32'
    ? `${process.env.windir || 'C:\\WINDOWS'}\\system32\\drivers\\etc\\hosts`
    : '/etc/hosts'
}


================================================
FILE: src/main/actions/hosts/getSystemHosts.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import getPathOfSystemHosts from './getPathOfSystemHostsPath'
import * as fs from 'fs'
import { normalizeLineEndings } from '@common/newlines'

export default async (): Promise<string> => {
  const fn = await getPathOfSystemHosts()

  if (!fs.existsSync(fn)) {
    return ''
  }

  return normalizeLineEndings(await fs.promises.readFile(fn, 'utf-8'))
}


================================================
FILE: src/main/actions/hosts/refresh.ts
================================================
/**
 * refreshHosts
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { getHostsContent, setHostsContent, setList } from '@main/actions/index'
import { broadcast } from '@main/core/agent'

import { swhdb } from '@main/data'
import { GET } from '@main/libs/request'
import { IHostsListObject, IOperationResult } from '@common/data'
import events from '@common/events'
import * as hostsFn from '@common/hostsFn'
import dayjs from 'dayjs'
import * as fs from 'fs'
import { URL } from 'url'

export default async (hosts_id: string): Promise<IOperationResult> => {
  let list = await swhdb.list.tree.all()
  let hosts: IHostsListObject | undefined = hostsFn.findItemById(list, hosts_id)

  if (!hosts) {
    return {
      success: false,
      code: 'invalid_id',
    }
  }

  let { type, url } = hosts

  if (type !== 'remote') {
    return {
      success: false,
      code: 'not_remote',
    }
  }

  if (!url) {
    return {
      success: false,
      code: 'no_url',
    }
  }

  let old_content: string = await getHostsContent(hosts.id)
  let new_content: string
  try {
    console.log(`-> refreshHosts URL: "${url}"`)
    if (url.startsWith('file://')) {
      new_content = await fs.promises.readFile(new URL(url), 'utf-8')
    } else {
      let resp = await GET(url)
      new_content = resp.data
    }
  } catch (e: any) {
    console.error(e)
    return {
      success: false,
      message: e.message,
    }
  }

  hosts.last_refresh = dayjs().format('YYYY-MM-DD HH:mm:ss')
  hosts.last_refresh_ms = new Date().getTime()

  await setList(list)

  if (old_content !== new_content) {
    await setHostsContent(hosts_id, new_content)
    broadcast(events.hosts_refreshed, hosts)
    broadcast(events.hosts_content_changed, hosts_id)
  }

  return {
    success: true,
    data: { ...hosts },
  }
}


================================================
FILE: src/main/actions/hosts/setContent.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { swhdb } from '@main/data'
import { IHostsContentObject } from '@common/data'
import { normalizeLineEndings } from '@common/newlines'

export default async (id: string, content: string) => {
  const normalizedContent = normalizeLineEndings(content)
  let d = await swhdb.collection.hosts.find<IHostsContentObject>((i) => i.id === id)
  if (!d || !d._id) {
    await swhdb.collection.hosts.insert({ id, content: normalizedContent })
  } else {
    await swhdb.collection.hosts.update((i) => i._id === d?._id, { content: normalizedContent })
  }
}


================================================
FILE: src/main/actions/hosts/setSystemHosts.ts
================================================
/**
 * @author: oldj
 * @homepage: https://oldj.net
 */

import { configGet, deleteHistory, getHistoryList, updateTrayTitle } from '@main/actions'
import tryToRun from '@main/actions/cmd/tryToRun'
import { broadcast } from '@main/core/agent'
import { swhdb } from '@main/data'
import safePSWD from '@main/libs/safePSWD'
import { IHostsWriteOptions } from '@main/types'
import { IHostsHistoryObject } from '@common/data'
import events from '@common/events'
import { getLineEndingForPlatform, normalizeLineEndings, restoreLineEndings } from '@common/newlines'
import { exec } from 'child_process'
import * as fs from 'fs'
import md5 from 'md5'
import md5File from 'md5-file'
import * as os from 'os'
import * as path from 'path'
import { v4 as uuid4 } from 'uuid'
import getPathOfSystemHosts from './getPathOfSystemHostsPath'

interface IWriteResult {
  success: boolean
  code?: string
  message?: string
  old_content?: string
  new_content?: string
}

const CONTENT_START = '# --- SWITCHHOSTS_CONTENT_START ---'

let sudo_pswd: string = ''

const checkAccess = async (fn: string): Promise<boolean> => {
  try {
    await fs.promises.access(fn, fs.constants.W_OK)
    return true
  } catch (e) {
    // console.error(e)
  }
  return false
}

const addHistory = async (content: string) => {
  await swhdb.collection.history.insert({
    id: uuid4(),
    content,
    add_time_ms: new Date().getTime(),
  })

  let history_limit = await configGet('history_limit')
  if (typeof history_limit !== 'number' || history_limit <= 0) return

  let lists = await swhdb.collection.history.all<IHostsHistoryObject>()
  if (lists.length <= history_limit) {
    return
  }

  for (let i = 0; i < lists.length - history_limit; i++) {
    if (!lists[i] || !lists[i].id) break
    await deleteHistory(lists[i].id)
  }
}

const writeWithSudo = (sys_hosts_path: string, content: string): Promise<IWriteResult> =>
  new Promise((resolve) => {
    let tmp_fn = path.join(os.tmpdir(), `swh_${new Date().getTime()}_${Math.random()}.txt`)
    fs.writeFileSync(tmp_fn, content, 'utf-8')

    let cmd = [
      `echo '$
Download .txt
gitextract_xbk83j9u/

├── .gitattributes
├── .github/
│   └── issue_template.md
├── .gitignore
├── .prettierignore
├── .prettierrc.json
├── .vscode/
│   └── settings.json
├── LICENSE
├── README.md
├── README.pl.md
├── README.zh_hans.md
├── README.zh_hant.md
├── alfred/
│   ├── Readme.txt
│   └── info.plist
├── app/
│   └── package.json
├── assets/
│   └── app.icns
├── package.json
├── scripts/
│   ├── entitlements.mac.plist
│   ├── hooks/
│   │   ├── artifactBuildCompleted.mjs
│   │   └── notarize-options.mjs
│   ├── libs/
│   │   ├── build-env.mjs
│   │   ├── build-log.mjs
│   │   ├── build-plan.mjs
│   │   ├── build-state.mjs
│   │   └── my-exec.mjs
│   ├── make.mjs
│   ├── release-config.mjs
│   ├── upload-diagnostics.mjs
│   ├── upload-progress.mjs
│   ├── upload-release.mjs
│   ├── vars.mjs
│   └── version-up.mjs
├── src/
│   ├── common/
│   │   ├── acknowledgements.ts
│   │   ├── constants.ts
│   │   ├── data.d.ts
│   │   ├── default_configs.ts
│   │   ├── events.ts
│   │   ├── hostsFn.ts
│   │   ├── i18n/
│   │   │   ├── index.ts
│   │   │   └── languages/
│   │   │       ├── de.ts
│   │   │       ├── en.ts
│   │   │       ├── fr.ts
│   │   │       ├── ja.ts
│   │   │       ├── ko.ts
│   │   │       ├── pl.ts
│   │   │       ├── tr.ts
│   │   │       ├── zh-hant.ts
│   │   │       └── zh.ts
│   │   ├── newlines.ts
│   │   ├── normalize.ts
│   │   ├── tree.ts
│   │   ├── types.d.ts
│   │   ├── update.ts
│   │   └── utils/
│   │       └── wait.ts
│   ├── main/
│   │   ├── actions/
│   │   │   ├── checkUpdate.ts
│   │   │   ├── closeMainWindow.ts
│   │   │   ├── cmd/
│   │   │   │   ├── changeDataDir.ts
│   │   │   │   ├── clearHistory.ts
│   │   │   │   ├── deleteHistory.ts
│   │   │   │   ├── focusMainWindow.ts
│   │   │   │   ├── getHistoryList.ts
│   │   │   │   ├── toggleDevTools.ts
│   │   │   │   └── tryToRun.ts
│   │   │   ├── config/
│   │   │   │   ├── all.ts
│   │   │   │   ├── get.ts
│   │   │   │   ├── set.ts
│   │   │   │   └── update.ts
│   │   │   ├── downloadUpdate.ts
│   │   │   ├── find/
│   │   │   │   ├── addHistory.ts
│   │   │   │   ├── addReplaceHistory.ts
│   │   │   │   ├── findBy.ts
│   │   │   │   ├── findPositionsInContent.ts
│   │   │   │   ├── getHistory.ts
│   │   │   │   ├── getReplaceHistory.ts
│   │   │   │   ├── setHistory.ts
│   │   │   │   ├── setReplaceHistory.ts
│   │   │   │   ├── show.ts
│   │   │   │   └── splitContent.ts
│   │   │   ├── getBasicData.ts
│   │   │   ├── getDataDir.ts
│   │   │   ├── getDefaultDataDir.ts
│   │   │   ├── hosts/
│   │   │   │   ├── deleteHistory.ts
│   │   │   │   ├── getContent.ts
│   │   │   │   ├── getHistoryList.ts
│   │   │   │   ├── getPathOfSystemHostsPath.ts
│   │   │   │   ├── getSystemHosts.ts
│   │   │   │   ├── refresh.ts
│   │   │   │   ├── setContent.ts
│   │   │   │   └── setSystemHosts.ts
│   │   │   ├── index.ts
│   │   │   ├── installUpdate.ts
│   │   │   ├── list/
│   │   │   │   ├── getContentOfList.ts
│   │   │   │   ├── getItem.ts
│   │   │   │   ├── getList.ts
│   │   │   │   ├── moveItemToTrashcan.ts
│   │   │   │   ├── moveManyToTrashcan.ts
│   │   │   │   └── setList.ts
│   │   │   ├── migrate/
│   │   │   │   ├── checkIfMigration.ts
│   │   │   │   ├── export.ts
│   │   │   │   ├── import.ts
│   │   │   │   ├── importFromUrl.ts
│   │   │   │   ├── importV3Data.ts
│   │   │   │   └── migrateData.ts
│   │   │   ├── openUrl.ts
│   │   │   ├── ping.ts
│   │   │   ├── quit.ts
│   │   │   ├── showItemInFolder.ts
│   │   │   ├── trashcan/
│   │   │   │   ├── clear.ts
│   │   │   │   ├── deleteItem.ts
│   │   │   │   ├── getList.ts
│   │   │   │   └── restoreItem.ts
│   │   │   └── updateTrayTitle.ts
│   │   ├── core/
│   │   │   ├── agent.ts
│   │   │   ├── getI18N.ts
│   │   │   ├── message.ts
│   │   │   ├── popupMenu.ts
│   │   │   └── updater.ts
│   │   ├── data/
│   │   │   └── index.ts
│   │   ├── http/
│   │   │   ├── api/
│   │   │   │   ├── index.ts
│   │   │   │   ├── list.ts
│   │   │   │   └── toggle.ts
│   │   │   └── index.ts
│   │   ├── libs/
│   │   │   ├── cron.ts
│   │   │   ├── getConfigDir.ts
│   │   │   ├── getDataDir.ts
│   │   │   ├── getIndex.ts
│   │   │   ├── isDev.ts
│   │   │   ├── request.ts
│   │   │   ├── safePSWD.ts
│   │   │   └── tracer.ts
│   │   ├── main.ts
│   │   ├── preload.ts
│   │   ├── types.d.ts
│   │   ├── ui/
│   │   │   ├── checkSystemLocale.ts
│   │   │   ├── find.ts
│   │   │   ├── menu.ts
│   │   │   └── tray/
│   │   │       ├── index.ts
│   │   │       └── window.ts
│   │   └── utils/
│   │       └── fs2.ts
│   ├── renderer/
│   │   ├── common/
│   │   │   └── PageWrapper.tsx
│   │   ├── components/
│   │   │   ├── About/
│   │   │   │   ├── AboutContent.module.scss
│   │   │   │   ├── AboutContent.tsx
│   │   │   │   ├── index.module.scss
│   │   │   │   └── index.tsx
│   │   │   ├── BrowserLink.tsx
│   │   │   ├── EditHostsInfo.module.scss
│   │   │   ├── EditHostsInfo.tsx
│   │   │   ├── Editor/
│   │   │   │   ├── HostsEditor.module.scss
│   │   │   │   ├── HostsEditor.tsx
│   │   │   │   ├── hosts_highlight.test.ts
│   │   │   │   └── hosts_highlight.ts
│   │   │   ├── History.module.scss
│   │   │   ├── History.tsx
│   │   │   ├── HostsViewer.module.scss
│   │   │   ├── HostsViewer.tsx
│   │   │   ├── ItemIcon.tsx
│   │   │   ├── Lang.tsx
│   │   │   ├── LeftPanel/
│   │   │   │   ├── SystemHostsItem.module.scss
│   │   │   │   ├── SystemHostsItem.tsx
│   │   │   │   ├── Trashcan.module.scss
│   │   │   │   ├── Trashcan.tsx
│   │   │   │   ├── TrashcanItem.module.scss
│   │   │   │   ├── TrashcanItem.tsx
│   │   │   │   ├── index.module.scss
│   │   │   │   └── index.tsx
│   │   │   ├── List/
│   │   │   │   ├── ListItem.module.scss
│   │   │   │   ├── ListItem.tsx
│   │   │   │   ├── index.module.scss
│   │   │   │   └── index.tsx
│   │   │   ├── Loading.module.scss
│   │   │   ├── Loading.tsx
│   │   │   ├── MainPanel/
│   │   │   │   ├── index.module.scss
│   │   │   │   └── index.tsx
│   │   │   ├── Pref/
│   │   │   │   ├── Advanced.tsx
│   │   │   │   ├── Commands.tsx
│   │   │   │   ├── CommandsHistory.tsx
│   │   │   │   ├── General.tsx
│   │   │   │   ├── Proxy.tsx
│   │   │   │   ├── index.tsx
│   │   │   │   └── styles.module.scss
│   │   │   ├── SetWriteMode.module.scss
│   │   │   ├── SetWriteMode.tsx
│   │   │   ├── SideDrawer.tsx
│   │   │   ├── StatusBar.module.scss
│   │   │   ├── StatusBar.tsx
│   │   │   ├── SudoPasswordInput.module.scss
│   │   │   ├── SudoPasswordInput.tsx
│   │   │   ├── SwitchButton.module.scss
│   │   │   ├── SwitchButton.tsx
│   │   │   ├── TopBar/
│   │   │   │   ├── ConfigMenu.module.scss
│   │   │   │   ├── ConfigMenu.tsx
│   │   │   │   ├── ImportFromUrl.module.scss
│   │   │   │   ├── ImportFromUrl.tsx
│   │   │   │   ├── index.module.scss
│   │   │   │   └── index.tsx
│   │   │   ├── Transfer.module.scss
│   │   │   ├── Transfer.tsx
│   │   │   ├── Tree/
│   │   │   │   ├── Node.tsx
│   │   │   │   ├── Tree.tsx
│   │   │   │   ├── fn.ts
│   │   │   │   ├── index.tsx
│   │   │   │   └── style.module.scss
│   │   │   └── UpdateDialog.tsx
│   │   ├── core/
│   │   │   ├── PopupMenu.ts
│   │   │   ├── agent.ts
│   │   │   └── useOnBroadcast.ts
│   │   ├── index.html
│   │   ├── index.tsx
│   │   ├── models/
│   │   │   ├── useConfigs.ts
│   │   │   ├── useHostsData.ts
│   │   │   └── useI18n.ts
│   │   ├── pages/
│   │   │   ├── find.module.scss
│   │   │   ├── find.tsx
│   │   │   ├── index.module.scss
│   │   │   ├── index.tsx
│   │   │   ├── tray.module.scss
│   │   │   └── tray.tsx
│   │   ├── stores/
│   │   │   ├── configs.ts
│   │   │   ├── hosts_data.ts
│   │   │   └── i18n.ts
│   │   ├── styles/
│   │   │   ├── common.scss
│   │   │   ├── fn.scss
│   │   │   ├── global.scss
│   │   │   ├── scrollbar.scss
│   │   │   ├── themes/
│   │   │   │   ├── dark.scss
│   │   │   │   └── light.scss
│   │   │   └── var.scss
│   │   └── utils/
│   │       └── css-var.ts
│   └── version.json
├── test/
│   ├── _base.ts
│   ├── common/
│   │   ├── hostsFn.test.ts
│   │   ├── mock/
│   │   │   ├── normalize.001.input.hosts
│   │   │   └── normalize.001.output.hosts
│   │   ├── newlines.test.ts
│   │   └── normalize.test.ts
│   ├── main/
│   │   ├── basic.test.ts
│   │   ├── findInContent.test.ts
│   │   ├── http.test.ts
│   │   ├── setSystemHosts.test.ts
│   │   ├── splitContent.test.ts
│   │   └── trashcan.test.ts
│   ├── scripts/
│   │   ├── upload-diagnostics.test.ts
│   │   └── upload-progress.test.ts
│   └── setup.ts
├── tsconfig.json
├── typings.d.ts
├── vite.main.config.mts
├── vite.render.config.mts
└── vitest.config.mts
Download .txt
SYMBOL INDEX (325 symbols across 79 files)

FILE: scripts/hooks/artifactBuildCompleted.mjs
  function artifactBuildCompleted (line 6) | async function artifactBuildCompleted(context) {

FILE: scripts/hooks/notarize-options.mjs
  function getPasswordFromKeychain (line 4) | function getPasswordFromKeychain(account, service) {
  function prepareNotarizeEnv (line 21) | async function prepareNotarizeEnv(env = process.env) {
  function hasNotarizeCredentials (line 51) | function hasNotarizeCredentials(env = process.env) {
  function getNotarizeOptions (line 59) | async function getNotarizeOptions(appPath, env = process.env) {

FILE: scripts/libs/build-env.mjs
  function hasValue (line 1) | function hasValue(value) {
  function getFirstConfiguredEnv (line 5) | function getFirstConfiguredEnv(env, names) {
  function isEnvFlagEnabled (line 15) | function isEnvFlagEnabled(value) {

FILE: scripts/libs/build-log.mjs
  constant PLATFORM_LABELS (line 4) | const PLATFORM_LABELS = {
  constant PLATFORM_COLORS (line 10) | const PLATFORM_COLORS = {
  function formatTimestamp (line 16) | function formatTimestamp(date = new Date()) {
  function formatLogLine (line 20) | function formatLogLine(message) {
  function formatDuration (line 24) | function formatDuration(ms) {
  function logBanner (line 41) | function logBanner(message) {
  function logStep (line 45) | function logStep(message) {
  function logSuccess (line 49) | function logSuccess(message) {
  function logWarning (line 53) | function logWarning(message) {
  function logPlatform (line 57) | function logPlatform(platform, message) {

FILE: scripts/libs/build-plan.mjs
  constant PLATFORM_ORDER (line 5) | const PLATFORM_ORDER = ['mac', 'win', 'linux']
  function resolvePlatformName (line 7) | function resolvePlatformName(name) {
  function formatArch (line 20) | function formatArch(arch) {
  function getBuildPlan (line 28) | function getBuildPlan(makeFor, targetPlatformsConfigs) {
  function createBuildTracker (line 51) | function createBuildTracker({ plan, compression, macBuildState, winBuild...

FILE: scripts/libs/build-state.mjs
  function hasSigningIdentityEnv (line 4) | function hasSigningIdentityEnv(env = process.env) {
  function describeNotarizationSetup (line 8) | function describeNotarizationSetup(env = process.env) {
  function resolveMacBuildState (line 29) | async function resolveMacBuildState(plan, env = process.env) {
  function resolveWindowsBuildState (line 94) | function resolveWindowsBuildState(plan, env = process.env) {

FILE: scripts/libs/my-exec.mjs
  function myExec (line 3) | function myExec(cmd, ...args) {

FILE: scripts/make.mjs
  constant TARGET_PLATFORMS_CONFIGS (line 26) | const TARGET_PLATFORMS_CONFIGS = {
  constant WINDOWS_TIMESTAMP_SERVER (line 48) | const WINDOWS_TIMESTAMP_SERVER = 'http://rfc3161timestamp.globalsign.com...
  function createBuilderConfig (line 50) | function createBuilderConfig(hooks, macBuildState, winBuildState) {
  function main (line 289) | async function main() {

FILE: scripts/release-config.mjs
  constant DEFAULT_GITHUB_REPOSITORY (line 6) | const DEFAULT_GITHUB_REPOSITORY = 'oldj/SwitchHosts'
  function getReleaseVersion (line 8) | function getReleaseVersion() {
  function getFullVersion (line 12) | function getFullVersion() {
  function getReleaseTag (line 16) | function getReleaseTag(env = process.env) {
  function resolveGithubRepository (line 29) | function resolveGithubRepository(env = process.env) {
  function isReleaseArtifactFile (line 49) | function isReleaseArtifactFile(fileName, fullVersion = getFullVersion()) {

FILE: scripts/upload-diagnostics.mjs
  function getCauseField (line 1) | function getCauseField(cause, field) {
  function normalizeTarget (line 7) | function normalizeTarget(target) {
  function getCause (line 30) | function getCause(error) {
  function pickEnumerableFields (line 42) | function pickEnumerableFields(value) {
  function extractErrorDetails (line 55) | function extractErrorDetails(error) {
  function buildDiagnostic (line 72) | function buildDiagnostic({
  function formatDiagnosticSummary (line 110) | function formatDiagnosticSummary(diagnostic) {
  function formatRetrySummary (line 135) | function formatRetrySummary(diagnostic, delayLabel) {
  function buildDebugPayload (line 156) | function buildDebugPayload(diagnostic, error) {
  function attachDiagnostic (line 170) | function attachDiagnostic(error, diagnostic) {

FILE: scripts/upload-progress.mjs
  constant PROGRESS_BAR_FORMAT (line 4) | const PROGRESS_BAR_FORMAT = '[:bar]'
  constant PROGRESS_BAR_WIDTH (line 5) | const PROGRESS_BAR_WIDTH = 24
  function clamp (line 7) | function clamp(value, min, max) {
  function formatPercent (line 11) | function formatPercent(value) {
  function formatEta (line 15) | function formatEta(seconds) {
  function truncateFileName (line 32) | function truncateFileName(fileName, maxLength = 36) {
  function formatProgressMessage (line 49) | function formatProgressMessage(snapshot) {
  function fitFileNameToWidth (line 61) | function fitFileNameToWidth(fileName, availableWidth, fallbackMaxLength ...
  function formatTtyProgressLines (line 77) | function formatTtyProgressLines(snapshot, barText, columns) {
  function buildSnapshot (line 95) | function buildSnapshot(state, now) {
  function createCaptureStream (line 132) | function createCaptureStream(columns = 120) {
  function createUploadProgressTracker (line 156) | function createUploadProgressTracker({

FILE: scripts/upload-release.mjs
  function log (line 52) | function log(message) {
  function logFileList (line 56) | function logFileList(files) {
  function getArtifactVersion (line 63) | function getArtifactVersion(fileName) {
  function sleep (line 68) | function sleep(ms) {
  function getRetryDelayMs (line 74) | function getRetryDelayMs(attempt) {
  function formatRetryDelay (line 78) | function formatRetryDelay(ms) {
  function isRetryableStatus (line 82) | function isRetryableStatus(status) {
  function isRetryableFetchError (line 86) | function isRetryableFetchError(error) {
  function getProgressSnapshot (line 109) | function getProgressSnapshot(progressTracker) {
  function logDiagnosticDebug (line 113) | function logDiagnosticDebug(error) {
  function readReleaseFiles (line 124) | async function readReleaseFiles() {
  function githubRequest (line 160) | async function githubRequest(
  function findReleaseByTag (line 238) | async function findReleaseByTag() {
  function createDraftRelease (line 265) | async function createDraftRelease() {
  function getUploadUrl (line 282) | function getUploadUrl(release) {
  function deleteAsset (line 286) | async function deleteAsset(assetId, assetName) {
  function tryDeleteAssetByName (line 294) | async function tryDeleteAssetByName(releaseId, assetName) {
  function uploadAsset (line 309) | async function uploadAsset(uploadUrl, file, { fileIndex, releaseId, prog...
  function main (line 425) | async function main() {

FILE: scripts/vars.mjs
  constant APP_NAME (line 9) | const APP_NAME = 'SwitchHosts'

FILE: src/common/data.d.ts
  type HostsType (line 3) | type HostsType = 'local' | 'remote' | 'group' | 'folder'
  type FolderModeType (line 4) | type FolderModeType = 0 | 1 | 2 // 0: 默认; 1: 单选; 2: 多选
  type IHostsListObject (line 6) | interface IHostsListObject {
  type IHostsContentObject (line 31) | interface IHostsContentObject {
  type ITrashcanObject (line 38) | interface ITrashcanObject {
  type ITrashcanListObject (line 44) | interface ITrashcanListObject extends ITrashcanObject, ITreeNodeData {
  type IHostsHistoryObject (line 53) | interface IHostsHistoryObject {
  type VersionType (line 60) | type VersionType = [number, number, number, number]
  type IHostsBasicData (line 62) | interface IHostsBasicData {
  type IOperationResult (line 68) | interface IOperationResult {
  type ICommandRunResult (line 75) | interface ICommandRunResult {

FILE: src/common/default_configs.ts
  type WriteModeType (line 4) | type WriteModeType = null | 'overwrite' | 'append'
  type ThemeType (line 5) | type ThemeType = 'light' | 'dark' | 'system'
  type ProtocolType (line 6) | type ProtocolType = 'http' | 'https'
  type DefaultLocaleType (line 7) | type DefaultLocaleType = LocaleName | undefined
  type ConfigsType (line 44) | type ConfigsType = typeof configs

FILE: src/common/hostsFn.ts
  type PartHostsObjectType (line 9) | type PartHostsObjectType = Partial<IHostsListObject> & { id: string }
  type Predicate (line 11) | type Predicate = (obj: IHostsListObject) => boolean

FILE: src/common/i18n/index.ts
  type LocaleName (line 33) | type LocaleName = keyof typeof languages
  class I18N (line 35) | class I18N {
    method constructor (line 39) | constructor(locale: LocaleName = 'en') {
    method trans (line 54) | trans(key: LanguageKey, words?: string[]) {

FILE: src/common/newlines.ts
  type LineEnding (line 1) | type LineEnding = '\n' | '\r\n'
  constant LINE_ENDING_RE (line 3) | const LINE_ENDING_RE = /\r\n?/g
  function normalizeLineEndings (line 5) | function normalizeLineEndings(content: string): string {
  function getLineEndingForPlatform (line 9) | function getLineEndingForPlatform(platform = process.platform): LineEndi...
  function restoreLineEndings (line 17) | function restoreLineEndings(content: string, lineEnding: LineEnding): st...

FILE: src/common/normalize.ts
  type INormalizeOptions (line 13) | type INormalizeOptions = Partial<typeof default_options>
  type IHostsLineObj (line 15) | interface IHostsLineObj {
  type IDomainsIPMap (line 21) | interface IDomainsIPMap {

FILE: src/common/tree.ts
  type NodeIdType (line 1) | type NodeIdType = string
  type ITreeNodeData (line 3) | interface ITreeNodeData {
  type IWithChildren (line 17) | interface IWithChildren {
  function flatten (line 21) | function flatten<T extends IWithChildren>(tree_list: T[]): T[] {
  type IWidthId (line 39) | interface IWidthId extends IWithChildren {
  function getNodeById (line 43) | function getNodeById<T extends IWidthId>(

FILE: src/common/types.d.ts
  type LanguageDict (line 12) | type LanguageDict = typeof lang
  type LanguageKey (line 13) | type LanguageKey = keyof LanguageDict
  type IActionFunc (line 15) | interface IActionFunc {
  type Actions (line 19) | type Actions = typeof actions & IActionFunc
  type IMenuItemOption (line 21) | interface IMenuItemOption extends MenuItemConstructorOptions {
  type IPopupMenuOption (line 27) | interface IPopupMenuOption {
  type IFindPosition (line 32) | interface IFindPosition {
  type IFindSplitter (line 44) | interface IFindSplitter {
  type IFindItem (line 51) | interface IFindItem {
  type IFindShowSourceParam (line 59) | type IFindShowSourceParam = IFindPosition & {

FILE: src/common/update.ts
  type AppUpdateInfo (line 1) | interface AppUpdateInfo {
  type AppUpdateProgress (line 7) | interface AppUpdateProgress {
  type AppDownloadedUpdateInfo (line 14) | interface AppDownloadedUpdateInfo extends AppUpdateInfo {

FILE: src/main/actions/find/addHistory.ts
  constant MAX_LENGTH (line 9) | const MAX_LENGTH = 20

FILE: src/main/actions/find/addReplaceHistory.ts
  constant MAX_LENGTH (line 9) | const MAX_LENGTH = 20

FILE: src/main/actions/find/findBy.ts
  type IFindOptions (line 13) | interface IFindOptions {

FILE: src/main/actions/find/findPositionsInContent.ts
  type MatchResult (line 8) | type MatchResult = Pick<

FILE: src/main/actions/find/setHistory.ts
  type IFindHistoryData (line 8) | interface IFindHistoryData {

FILE: src/main/actions/find/splitContent.ts
  type MatchResult (line 8) | type MatchResult = Pick<IFindPosition, 'start' | 'end' | 'match'> & {

FILE: src/main/actions/hosts/setSystemHosts.ts
  type IWriteResult (line 24) | interface IWriteResult {
  constant CONTENT_START (line 32) | const CONTENT_START = '# --- SWITCHHOSTS_CONTENT_START ---'

FILE: src/main/core/message.ts
  function sendBack (line 60) | function sendBack(sender: any, event_name: string, data: [any] | [any, a...

FILE: src/main/core/updater.ts
  function normalizeReleaseNotes (line 11) | function normalizeReleaseNotes(releaseNotes: UpdateInfo['releaseNotes'])...
  function toAppUpdateInfo (line 36) | function toAppUpdateInfo(info: UpdateInfo): AppUpdateInfo {
  function toProgressPayload (line 44) | function toProgressPayload(info: ProgressInfo): AppUpdateProgress {
  function toDownloadedUpdateInfo (line 53) | function toDownloadedUpdateInfo(event: UpdateDownloadedEvent): AppDownlo...
  function bindUpdaterEvents (line 60) | function bindUpdaterEvents() {
  function checkUpdate (line 102) | async function checkUpdate(): Promise<AppUpdateInfo | null> {
  function downloadUpdate (line 119) | async function downloadUpdate() {
  function installUpdate (line 130) | async function installUpdate() {

FILE: src/main/data/index.ts
  function getSwhDb (line 35) | async function getSwhDb(): Promise<PotDb> {

FILE: src/main/libs/getDataDir.ts
  function getDefaultDataDir (line 9) | function getDefaultDataDir() {

FILE: src/main/libs/request.ts
  type IParams (line 12) | interface IParams {
  type IRequestOptions (line 16) | interface IRequestOptions {

FILE: src/main/libs/tracer.ts
  class Tracer (line 5) | class Tracer {
    method constructor (line 8) | constructor() {
    method add (line 12) | add(action: string) {
    method emit (line 16) | async emit() {

FILE: src/main/preload.ts
  type Window (line 13) | interface Window {
  type EventHandler (line 18) | type EventHandler = (...args: any[]) => void

FILE: src/main/types.d.ts
  type ActionData (line 13) | interface ActionData {
  type IHostsWriteOptions (line 19) | interface IHostsWriteOptions {

FILE: src/main/ui/menu.ts
  method click (line 95) | click() {
  method click (line 102) | click() {
  method click (line 114) | click(_item: MenuItem, focusedWindow) {
  method click (line 122) | click(_item: MenuItem, focusedWindow) {
  method click (line 180) | click() {
  method click (line 186) | click() {

FILE: src/main/ui/tray/index.ts
  method click (line 100) | click() {
  method click (line 113) | async click() {

FILE: src/renderer/common/PageWrapper.tsx
  type IProps (line 9) | interface IProps {
  function PageWrapper (line 13) | function PageWrapper(props: IProps) {

FILE: src/renderer/components/BrowserLink.tsx
  type Props (line 10) | interface Props {

FILE: src/renderer/components/Editor/hosts_highlight.ts
  constant HOSTS_LINE_RE (line 15) | const HOSTS_LINE_RE = /^\s*([\d.]+|[\da-f:.%lo]+)\s+\w/i
  constant COMMENT_LINE_RE (line 17) | const COMMENT_LINE_RE = /^(\s*)#\s*/
  type LineInfo (line 20) | interface LineInfo {
  type InsertTransform (line 31) | interface InsertTransform {
  type RemoveTransform (line 37) | interface RemoveTransform {
  type Transform (line 43) | type Transform = InsertTransform | RemoveTransform
  type CommentToggleResult (line 45) | interface CommentToggleResult {
  type ToggleLineResult (line 52) | interface ToggleLineResult {
  function escapeHtml (line 58) | function escapeHtml(text: string): string {
  function isHostsCommentLine (line 67) | function isHostsCommentLine(line: string): boolean {
  function isValidHostsLine (line 71) | function isValidHostsLine(line: string): boolean {
  function highlightHostsLine (line 75) | function highlightHostsLine(line: string): string {
  function highlightHostsText (line 96) | function highlightHostsText(code: string): string {
  function highlightHosts (line 104) | function highlightHosts(editor: HTMLElement, _pos?: Position): void {
  function getLines (line 108) | function getLines(code: string): LineInfo[] {
  function getLineIndexAtOffset (line 123) | function getLineIndexAtOffset(lines: LineInfo[], offset: number): number {
  function toggleLine (line 135) | function toggleLine(line: string, lineStart: number): ToggleLineResult {
  function mapOffset (line 169) | function mapOffset(offset: number, transforms: Transform[]): number {
  function getLineStartOffsets (line 193) | function getLineStartOffsets(lines: string[]): number[] {
  function getSelectionRange (line 205) | function getSelectionRange(selectionStart: number, selectionEnd: number) {
  function toggleCommentLines (line 218) | function toggleCommentLines(
  function toggleCommentBySelection (line 272) | function toggleCommentBySelection(
  function toggleCommentByLine (line 296) | function toggleCommentByLine(

FILE: src/renderer/components/History.tsx
  type IHistoryProps (line 32) | interface IHistoryProps {

FILE: src/renderer/components/HostsViewer.tsx
  type Props (line 9) | interface Props {

FILE: src/renderer/components/ItemIcon.tsx
  type Props (line 15) | interface Props {

FILE: src/renderer/components/Lang.tsx
  type Props (line 10) | interface Props {

FILE: src/renderer/components/LeftPanel/TrashcanItem.tsx
  type Props (line 16) | interface Props {
  method click (line 33) | click() {
  method click (line 47) | click() {
  method click (line 58) | click() {

FILE: src/renderer/components/LeftPanel/index.tsx
  type Props (line 15) | interface Props {
  method click (line 26) | click() {

FILE: src/renderer/components/List/ListItem.tsx
  type Props (line 23) | interface Props {
  method click (line 98) | click() {
  method click (line 104) | async click() {
  method click (line 137) | click() {

FILE: src/renderer/components/List/index.tsx
  type Props (line 24) | interface Props {

FILE: src/renderer/components/Pref/Advanced.tsx
  type IProps (line 13) | interface IProps {

FILE: src/renderer/components/Pref/Commands.tsx
  type IProps (line 12) | interface IProps {

FILE: src/renderer/components/Pref/CommandsHistory.tsx
  type Props (line 14) | interface Props {

FILE: src/renderer/components/Pref/General.tsx
  type IProps (line 13) | interface IProps {

FILE: src/renderer/components/Pref/Proxy.tsx
  type IProps (line 11) | interface IProps {

FILE: src/renderer/components/SetWriteMode.tsx
  type Props (line 16) | interface Props {}
  type IPendingData (line 18) | interface IPendingData {

FILE: src/renderer/components/SideDrawer.tsx
  type SideDrawerProps (line 5) | interface SideDrawerProps extends Omit<DrawerProps, 'children' | 'positi...

FILE: src/renderer/components/StatusBar.tsx
  type Props (line 11) | interface Props {

FILE: src/renderer/components/SwitchButton.tsx
  type Props (line 10) | interface Props {

FILE: src/renderer/components/TopBar/ConfigMenu.tsx
  type IProps (line 29) | interface IProps {

FILE: src/renderer/components/TopBar/ImportFromUrl.tsx
  type Props (line 13) | interface Props {

FILE: src/renderer/components/TopBar/index.tsx
  type IProps (line 25) | interface IProps {

FILE: src/renderer/components/Transfer.tsx
  type IdType (line 13) | type IdType = string
  type ITransferSourceObject (line 15) | interface ITransferSourceObject {
  type IListProps (line 21) | interface IListProps {
  type Props (line 27) | interface Props {

FILE: src/renderer/components/Tree/Node.tsx
  type Window (line 15) | interface Window {
  type NodeUpdate (line 21) | type NodeUpdate = (data: Partial<ITreeNodeData>) => void
  type INodeProps (line 23) | interface INodeProps {
  function diff (line 275) | function diff<T>(a: T[], b: T[]): T[] {
  function isEqual (line 279) | function isEqual(prevProps: INodeProps, nextProps: INodeProps): boolean {

FILE: src/renderer/components/Tree/Tree.tsx
  type DropWhereType (line 14) | type DropWhereType = 'before' | 'in' | 'after'
  type MultipleSelectType (line 15) | type MultipleSelectType = 0 | 1 | 2
  type ITreeProps (line 17) | interface ITreeProps {

FILE: src/renderer/components/Tree/fn.ts
  type IObj (line 5) | interface IObj {
  type KeyMapType (line 9) | type KeyMapType = [string, string]
  function flatten (line 11) | function flatten(tree_list: ITreeNodeData[]): ITreeNodeData[] {
  function getParentList (line 29) | function getParentList(tree_list: ITreeNodeData[], id: NodeIdType): ITre...
  function getNodeById (line 88) | function getNodeById(tree_list: ITreeNodeData[], id: NodeIdType): ITreeN...
  function isChildOf (line 95) | function isChildOf(tree_list: ITreeNodeData[], a_id: NodeIdType, b_id: N...
  function isSelfOrChild (line 104) | function isSelfOrChild(item: ITreeNodeData, id: NodeIdType | null): bool...
  function objKeyMap (line 110) | function objKeyMap(obj: IObj, key_maps: KeyMapType[], reversed: boolean ...
  function treeKeyMap (line 138) | function treeKeyMap(
  function keyMapReverse (line 150) | function keyMapReverse(key_maps: KeyMapType[]): KeyMapType[] {
  function isParent (line 154) | function isParent(tree_list: ITreeNodeData[], item: ITreeNodeData, id: s...
  function canBeSelected (line 159) | function canBeSelected(
  function selectTo (line 183) | function selectTo(

FILE: src/renderer/components/Tree/index.tsx
  type KeyMapType (line 10) | type KeyMapType = _KeyMapType

FILE: src/renderer/components/UpdateDialog.tsx
  type UpdateStage (line 9) | type UpdateStage = 'available' | 'downloading' | 'downloaded'

FILE: src/renderer/core/PopupMenu.ts
  type OffFunction (line 11) | type OffFunction = () => void
  class PopupMenu (line 13) | class PopupMenu {
    method constructor (line 18) | constructor(menu_items: IMenuItemOption[]) {
    method show (line 23) | show() {
    method onHide (line 56) | private onHide() {

FILE: src/renderer/core/agent.ts
  method get (line 11) | get(obj, key: keyof Actions) {

FILE: src/renderer/models/useConfigs.ts
  function useConfigs (line 11) | function useConfigs() {

FILE: src/renderer/models/useHostsData.ts
  function useHostsData (line 12) | function useHostsData() {

FILE: src/renderer/models/useI18n.ts
  function useI18n (line 10) | function useI18n() {

FILE: src/renderer/pages/find.tsx
  type IFindPositionShow (line 29) | interface IFindPositionShow extends IFindPosition {
  method click (line 295) | click() {
  method click (line 313) | click() {

FILE: test/_base.ts
  type Global (line 17) | interface Global {

FILE: test/main/setSystemHosts.test.ts
  method add (line 19) | add() {}

FILE: test/scripts/upload-progress.test.ts
  method log (line 16) | log() {}
  method log (line 46) | log() {}
  method log (line 62) | log() {}
  method log (line 86) | log() {}
  method log (line 115) | log(message) {
  class FakeProgressBar (line 141) | class FakeProgressBar {
    method interrupt (line 146) | interrupt(message: string) {
    method render (line 150) | render() {}
    method terminate (line 152) | terminate() {}
    method tick (line 154) | tick(delta: number) {
    method update (line 158) | update(ratio: number) {
  method clearLine (line 164) | clearLine() {}
  method cursorTo (line 165) | cursorTo() {}
  method moveCursor (line 167) | moveCursor() {}
  method write (line 168) | write(chunk: string) {
  method log (line 177) | log(message) {

FILE: test/setup.ts
  class BrowserWindowMock (line 18) | class BrowserWindowMock {
    method fromWebContents (line 19) | static fromWebContents() {
    method closeDevTools (line 28) | closeDevTools() {}
    method openDevTools (line 29) | openDevTools() {}
    method once (line 30) | once() {}
    method setPermissionRequestHandler (line 32) | setPermissionRequestHandler() {}
    method onBeforeSendHeaders (line 34) | onBeforeSendHeaders() {}
    method onHeadersReceived (line 35) | onHeadersReceived() {}
    method toggleDevTools (line 38) | toggleDevTools() {}
    method constructor (line 41) | constructor(options?: { width?: number; height?: number }) {
    method focus (line 46) | focus() {
    method getBounds (line 50) | getBounds() {
    method hide (line 54) | hide() {
    method isFocused (line 59) | isFocused() {
    method isVisible (line 63) | isVisible() {
    method loadURL (line 67) | loadURL() {
    method on (line 71) | on() {
    method setPosition (line 75) | setPosition(x: number, y: number) {
    method setVisibleOnAllWorkspaces (line 80) | setVisibleOnAllWorkspaces() {}
    method show (line 82) | show() {
  method getPath (line 90) | getPath(name: string) {
  method quit (line 98) | quit() {}
  method whenReady (line 99) | whenReady() {
  method hide (line 103) | hide() {}
  method show (line 104) | show() {
  method buildFromTemplate (line 111) | buildFromTemplate() {
  method setToolTip (line 117) | setToolTip() {}
  method setContextMenu (line 118) | setContextMenu() {}
  method on (line 119) | on() {}
  method popUpContextMenu (line 120) | popUpContextMenu() {}
  method getBounds (line 121) | getBounds() {
  method getCursorScreenPoint (line 126) | getCursorScreenPoint() {
  method getDisplayNearestPoint (line 129) | getDisplayNearestPoint() {
  method openExternal (line 137) | openExternal() {
  method showItemInFolder (line 140) | showItemInFolder() {
  method showOpenDialog (line 145) | showOpenDialog() {
  method showSaveDialog (line 148) | showSaveDialog() {
  method emit (line 153) | emit() {}
  method on (line 154) | on() {}
  method handle (line 155) | handle() {}
  method removeHandler (line 156) | removeHandler() {}
  method removeAllListeners (line 157) | removeAllListeners() {}
  method invoke (line 160) | invoke() {
  method on (line 163) | on() {}
  method send (line 164) | send() {}
  method removeAllListeners (line 165) | removeAllListeners() {}
  method exposeInMainWorld (line 168) | exposeInMainWorld() {}

FILE: typings.d.ts
  type ReactSVG (line 10) | interface ReactSVG {
Condensed preview — 248 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (514K chars).
[
  {
    "path": ".gitattributes",
    "chars": 107,
    "preview": "legacy/* linguist-vendored\nscripts/alfred/workflow/* linguist-vendored\n\n*.css linguist-language=javascript\n"
  },
  {
    "path": ".github/issue_template.md",
    "chars": 147,
    "preview": "### System (Mac, Windows 10/11, Linux) / 操作系统\n\n\n\n### SwitchHosts Version / SwitchHosts 版本\n\n\n\n### Description / 描述\n\n\n\n###"
  },
  {
    "path": ".gitignore",
    "chars": 324,
    "preview": ".idea\n\n# dependencies\nnode_modules\nnpm-debug.log*\nyarn-error.log\nyarn.lock\npackage-lock.json\n\n# production\nbuild\ndist\n\n#"
  },
  {
    "path": ".prettierignore",
    "chars": 47,
    "preview": "src/renderer/.umi\nsrc/renderer/.umi-production\n"
  },
  {
    "path": ".prettierrc.json",
    "chars": 444,
    "preview": "{\n  \"arrowParens\": \"always\",\n  \"bracketSpacing\": true,\n  \"embeddedLanguageFormatting\": \"auto\",\n  \"endOfLine\": \"lf\",\n  \"h"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 544,
    "preview": "{\n  \"editor.formatOnSave\": true,\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  \"editor.codeActionsOnSave\": {\n"
  },
  {
    "path": "LICENSE",
    "chars": 11339,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "README.md",
    "chars": 2925,
    "preview": "<div align=\"center\" markdown=\"1\">\n  <sup>Special thanks to:</sup>\n  <br>\n  <a href=\"https://go.warp.dev/SwitchHosts\">\n  "
  },
  {
    "path": "README.pl.md",
    "chars": 3038,
    "preview": "<div align=\"center\" markdown=\"1\">\n  <sup>Special thanks to:</sup>\n  <br>\n  <a href=\"https://go.warp.dev/SwitchHosts\">\n  "
  },
  {
    "path": "README.zh_hans.md",
    "chars": 2143,
    "preview": "<div align=\"center\" markdown=\"1\">\n  <sup>Special thanks to:</sup>\n  <br>\n  <a href=\"https://go.warp.dev/SwitchHosts\">\n  "
  },
  {
    "path": "README.zh_hant.md",
    "chars": 2098,
    "preview": "<div align=\"center\" markdown=\"1\">\n  <sup>Special thanks to:</sup>\n  <br>\n  <a href=\"https://go.warp.dev/SwitchHosts\">\n  "
  },
  {
    "path": "alfred/Readme.txt",
    "chars": 98,
    "preview": "SwitchHosts! is an App for switching hosts quickly.\n\nHomepage: https://oldj.github.io/SwitchHosts/"
  },
  {
    "path": "alfred/info.plist",
    "chars": 4600,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "app/package.json",
    "chars": 387,
    "preview": "{\n  \"name\": \"switchhosts\",\n  \"productName\": \"SwitchHosts\",\n  \"version\": \"4.3.0.6137\",\n  \"description\": \"Switch hosts qui"
  },
  {
    "path": "package.json",
    "chars": 3300,
    "preview": "{\n  \"private\": true,\n  \"scripts\": {\n    \"start\": \"cross-env NODE_ENV=development electron ./build/main.js\",\n    \"pretest"
  },
  {
    "path": "scripts/entitlements.mac.plist",
    "chars": 353,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "scripts/hooks/artifactBuildCompleted.mjs",
    "chars": 932,
    "preview": "import { notarize } from '@electron/notarize'\nimport path from 'node:path'\nimport { isEnvFlagEnabled } from '../libs/bui"
  },
  {
    "path": "scripts/hooks/notarize-options.mjs",
    "chars": 2857,
    "preview": "import { execFile } from 'node:child_process'\nimport { isEnvFlagEnabled } from '../libs/build-env.mjs'\n\nfunction getPass"
  },
  {
    "path": "scripts/libs/build-env.mjs",
    "chars": 661,
    "preview": "export function hasValue(value) {\n  return typeof value === 'string' ? value.trim() !== '' : Boolean(value)\n}\n\nexport fu"
  },
  {
    "path": "scripts/libs/build-log.mjs",
    "chars": 1437,
    "preview": "import chalk from 'chalk'\nimport dayjs from 'dayjs'\n\nexport const PLATFORM_LABELS = {\n  mac: 'macOS',\n  win: 'Windows',\n"
  },
  {
    "path": "scripts/libs/build-plan.mjs",
    "chars": 4702,
    "preview": "import { Arch } from 'builder-util'\nimport path from 'node:path'\nimport { PLATFORM_LABELS, formatDuration, logBanner, lo"
  },
  {
    "path": "scripts/libs/build-state.mjs",
    "chars": 4388,
    "preview": "import { hasNotarizeCredentials, prepareNotarizeEnv } from '../hooks/notarize-options.mjs'\nimport { getFirstConfiguredEn"
  },
  {
    "path": "scripts/libs/my-exec.mjs",
    "chars": 639,
    "preview": "import { spawn } from 'node:child_process'\n\nexport default function myExec(cmd, ...args) {\n  return new Promise((resolve"
  },
  {
    "path": "scripts/make.mjs",
    "chars": 10474,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport chalk from 'chalk'\nimport { config as loadEnv } from 'do"
  },
  {
    "path": "scripts/release-config.mjs",
    "chars": 1796,
    "preview": "import { createRequire } from 'node:module'\n\nconst require = createRequire(import.meta.url)\nconst version = require('../"
  },
  {
    "path": "scripts/upload-diagnostics.mjs",
    "chars": 4831,
    "preview": "function getCauseField(cause, field) {\n  if (!cause || !(field in cause)) return null\n  const value = cause[field]\n  ret"
  },
  {
    "path": "scripts/upload-progress.mjs",
    "chars": 10394,
    "preview": "import prettyBytes from 'pretty-bytes'\nimport ProgressBar from 'progress'\n\nconst PROGRESS_BAR_FORMAT = '[:bar]'\nconst PR"
  },
  {
    "path": "scripts/upload-release.mjs",
    "chars": 14754,
    "preview": "import chalk from 'chalk'\nimport { config as loadEnv } from 'dotenv'\nimport { createReadStream, promises as fs } from 'n"
  },
  {
    "path": "scripts/vars.mjs",
    "chars": 453,
    "preview": "import path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst __filename = fileURLToPath(import.meta.url)"
  },
  {
    "path": "scripts/version-up.mjs",
    "chars": 920,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { "
  },
  {
    "path": "src/common/acknowledgements.ts",
    "chars": 2270,
    "preview": "/**\n * acknowledgements\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nexport default [\n  { name: 'oldj', link: 'h"
  },
  {
    "path": "src/common/constants.ts",
    "chars": 405,
    "preview": "/**\n * constants\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nexport const server_url = 'https://switchhosts.ver"
  },
  {
    "path": "src/common/data.d.ts",
    "chars": 1504,
    "preview": "import { ITreeNodeData } from './tree'\n\nexport type HostsType = 'local' | 'remote' | 'group' | 'folder'\nexport type Fold"
  },
  {
    "path": "src/common/default_configs.ts",
    "chars": 1246,
    "preview": "import { LocaleName } from '@common/i18n'\nimport { FolderModeType } from './data.d'\n\nexport type WriteModeType = null | "
  },
  {
    "path": "src/common/events.ts",
    "chars": 1258,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nexport default {\n  active_main_window: 'active_main_window',\n  "
  },
  {
    "path": "src/common/hostsFn.ts",
    "chars": 5237,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { FolderModeType, IHostsBasicData, IHostsListObject } fr"
  },
  {
    "path": "src/common/i18n/index.ts",
    "chars": 1264,
    "preview": "/**\n * index\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport en from './languages/en'\nimport zh from './lang"
  },
  {
    "path": "src/common/i18n/languages/de.ts",
    "chars": 7738,
    "preview": "/**\n * @author: bergo\n * @homepage: https://bergo.dev\n */\n\nimport { LanguageDict } from '@common/types'\n\nconst lang: Lan"
  },
  {
    "path": "src/common/i18n/languages/en.ts",
    "chars": 6818,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nexport default {\n  _app_name: 'SwitchHosts',\n  _key: 'en',\n  _n"
  },
  {
    "path": "src/common/i18n/languages/fr.ts",
    "chars": 7850,
    "preview": "/**\n * @author: Aktilor\n * @homepage: https://github.com/Aktilor\n */\n\nimport { LanguageDict } from '@common/types'\n\ncons"
  },
  {
    "path": "src/common/i18n/languages/ja.ts",
    "chars": 5443,
    "preview": "/**\n * @author: kamatte\n * @homepage: https://kamatte.me\n */\n\nimport { LanguageDict } from '@common/types'\n\nconst lang: "
  },
  {
    "path": "src/common/i18n/languages/ko.ts",
    "chars": 5253,
    "preview": "/**\n * @author: wooklab\n */\n\nexport default {\n  _app_name: 'SwitchHosts',\n  _key: 'ko',\n  _name: '한국어',\n  about: '정보',\n "
  },
  {
    "path": "src/common/i18n/languages/pl.ts",
    "chars": 7141,
    "preview": "/**\n * @author: piteriuz\n * @homepage: https://piotr.pienkowski.pl/\n */\n\nexport default {\n  _app_name: 'SwitchHosts',\n  "
  },
  {
    "path": "src/common/i18n/languages/tr.ts",
    "chars": 7079,
    "preview": "/**\n * @author: baris\n * @homepage: https://barisuzun.com.tr\n */\n\nexport default {\n  _app_name: 'SwitchHosts',\n  _key: '"
  },
  {
    "path": "src/common/i18n/languages/zh-hant.ts",
    "chars": 4887,
    "preview": "/**\n * @author: rayatn1011\n * @homepage: https://github.com/rayatn1011\n */\n\nimport { LanguageDict } from '@common/types'"
  },
  {
    "path": "src/common/i18n/languages/zh.ts",
    "chars": 4859,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { LanguageDict } from '@common/types'\n\nconst lang: Langu"
  },
  {
    "path": "src/common/newlines.ts",
    "chars": 582,
    "preview": "export type LineEnding = '\\n' | '\\r\\n'\n\nconst LINE_ENDING_RE = /\\r\\n?/g\n\nexport function normalizeLineEndings(content: s"
  },
  {
    "path": "src/common/normalize.ts",
    "chars": 2150,
    "preview": "/**\n * normalize\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport * as os from 'os'\n\nconst default_options = "
  },
  {
    "path": "src/common/tree.ts",
    "chars": 1046,
    "preview": "export type NodeIdType = string\n\nexport interface ITreeNodeData {\n  id: NodeIdType\n  title?: string\n  can_select?: boole"
  },
  {
    "path": "src/common/types.d.ts",
    "chars": 1238,
    "preview": "/**\n * types\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { HostsType } from '@common/data'\nimport { Menu"
  },
  {
    "path": "src/common/update.ts",
    "chars": 336,
    "preview": "export interface AppUpdateInfo {\n  version: string\n  releaseName?: string | null\n  releaseNotes?: string | null\n}\n\nexpor"
  },
  {
    "path": "src/common/utils/wait.ts",
    "chars": 138,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nexport default (ms: number) => new Promise((resolve) => setTime"
  },
  {
    "path": "src/main/actions/checkUpdate.ts",
    "chars": 243,
    "preview": "import * as updater from '@main/core/updater'\n\nexport default async (): Promise<boolean | null> => {\n  try {\n    const u"
  },
  {
    "path": "src/main/actions/closeMainWindow.ts",
    "chars": 157,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nexport default async () => {\n  let win = global.main_win\n  win "
  },
  {
    "path": "src/main/actions/cmd/changeDataDir.ts",
    "chars": 1415,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { app, BrowserWindow, dialog, OpenDialogOptions, OpenDia"
  },
  {
    "path": "src/main/actions/cmd/clearHistory.ts",
    "chars": 177,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { cfgdb } from '@main/data'\n\nexport default async () => "
  },
  {
    "path": "src/main/actions/cmd/deleteHistory.ts",
    "chars": 208,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { cfgdb } from '@main/data'\n\nexport default async (_id: "
  },
  {
    "path": "src/main/actions/cmd/focusMainWindow.ts",
    "chars": 133,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nexport default () => {\n  global.main_win.show()\n  global.main_w"
  },
  {
    "path": "src/main/actions/cmd/getHistoryList.ts",
    "chars": 271,
    "preview": "/**\n * getHistoryList\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { cfgdb } from '@main/data'\nimport { I"
  },
  {
    "path": "src/main/actions/cmd/toggleDevTools.ts",
    "chars": 165,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nexport default () => {\n  let win = global.main_win\n  if (!win) "
  },
  {
    "path": "src/main/actions/cmd/tryToRun.ts",
    "chars": 1324,
    "preview": "/**\n * run\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { cfgdb } from '@main/data'\nimport { ICommandRunR"
  },
  {
    "path": "src/main/actions/config/all.ts",
    "chars": 441,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { cfgdb } from '@main/data'\nimport default_configs, { Co"
  },
  {
    "path": "src/main/actions/config/get.ts",
    "chars": 311,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { cfgdb } from '@main/data'\nimport default_configs, { Co"
  },
  {
    "path": "src/main/actions/config/set.ts",
    "chars": 328,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { cfgdb } from '@main/data'\nimport { ConfigsType } from "
  },
  {
    "path": "src/main/actions/config/update.ts",
    "chars": 1210,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { updateTrayTitle } from '@main/actions'\nimport { cfgdb "
  },
  {
    "path": "src/main/actions/downloadUpdate.ts",
    "chars": 143,
    "preview": "import * as updater from '@main/core/updater'\n\nexport default async (): Promise<boolean> => {\n  await updater.downloadUp"
  },
  {
    "path": "src/main/actions/find/addHistory.ts",
    "chars": 558,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport getHistory from '@main/actions/find/getHistory'\nimport s"
  },
  {
    "path": "src/main/actions/find/addReplaceHistory.ts",
    "chars": 559,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport getReplaceHistory from '@main/actions/find/getReplaceHis"
  },
  {
    "path": "src/main/actions/find/findBy.ts",
    "chars": 1418,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport splitContent from '@main/actions/find/splitContent'\nimpo"
  },
  {
    "path": "src/main/actions/find/findPositionsInContent.ts",
    "chars": 1315,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { IFindPosition } from '@common/types'\n\ntype MatchResult"
  },
  {
    "path": "src/main/actions/find/getHistory.ts",
    "chars": 287,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { IFindHistoryData } from '@main/actions/find/setHistory"
  },
  {
    "path": "src/main/actions/find/getReplaceHistory.ts",
    "chars": 205,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { cfgdb } from '@main/data'\n\nexport default async (): Pr"
  },
  {
    "path": "src/main/actions/find/setHistory.ts",
    "chars": 292,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { cfgdb } from '@main/data'\n\nexport interface IFindHisto"
  },
  {
    "path": "src/main/actions/find/setReplaceHistory.ts",
    "chars": 183,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { cfgdb } from '@main/data'\n\nexport default async (data:"
  },
  {
    "path": "src/main/actions/find/show.ts",
    "chars": 257,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { makeWindow } from '@main/ui/find'\n\nexport default asyn"
  },
  {
    "path": "src/main/actions/find/splitContent.ts",
    "chars": 752,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { IFindPosition, IFindSplitter } from '@common/types'\n\nt"
  },
  {
    "path": "src/main/actions/getBasicData.ts",
    "chars": 1192,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { swhdb } from '@main/data'\nimport { IHostsBasicData, IH"
  },
  {
    "path": "src/main/actions/getDataDir.ts",
    "chars": 145,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport getDataDir from '@main/libs/getDataDir'\n\nexport default "
  },
  {
    "path": "src/main/actions/getDefaultDataDir.ts",
    "chars": 163,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { getDefaultDataDir } from '@main/libs/getDataDir'\n\nexpo"
  },
  {
    "path": "src/main/actions/hosts/deleteHistory.ts",
    "chars": 256,
    "preview": "/**\n * removeHistory\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { swhdb } from '@main/data'\n\nexport def"
  },
  {
    "path": "src/main/actions/hosts/getContent.ts",
    "chars": 1641,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { configGet, getItemFromList, getList } from '@main/acti"
  },
  {
    "path": "src/main/actions/hosts/getHistoryList.ts",
    "chars": 401,
    "preview": "/**\n * getHistoryList\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { swhdb } from '@main/data'\nimport { I"
  },
  {
    "path": "src/main/actions/hosts/getPathOfSystemHostsPath.ts",
    "chars": 267,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nexport default async (): Promise<string> => {\n  // Windows 系统有可"
  },
  {
    "path": "src/main/actions/hosts/getSystemHosts.ts",
    "chars": 410,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport getPathOfSystemHosts from './getPathOfSystemHostsPath'\ni"
  },
  {
    "path": "src/main/actions/hosts/refresh.ts",
    "chars": 1818,
    "preview": "/**\n * refreshHosts\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { getHostsContent, setHostsContent, setL"
  },
  {
    "path": "src/main/actions/hosts/setContent.ts",
    "chars": 611,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { swhdb } from '@main/data'\nimport { IHostsContentObject"
  },
  {
    "path": "src/main/actions/hosts/setSystemHosts.ts",
    "chars": 5849,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { configGet, deleteHistory, getHistoryList, updateTrayTi"
  },
  {
    "path": "src/main/actions/index.ts",
    "chars": 3381,
    "preview": "/**\n * index\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nexport { default as ping } from './ping'\n\nexport { def"
  },
  {
    "path": "src/main/actions/installUpdate.ts",
    "chars": 142,
    "preview": "import * as updater from '@main/core/updater'\n\nexport default async (): Promise<boolean> => {\n  await updater.installUpd"
  },
  {
    "path": "src/main/actions/list/getContentOfList.ts",
    "chars": 848,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { configGet, getHostsContent } from '@main/actions'\nimpo"
  },
  {
    "path": "src/main/actions/list/getItem.ts",
    "chars": 334,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { getList } from '@main/actions'\nimport { IHostsListObje"
  },
  {
    "path": "src/main/actions/list/getList.ts",
    "chars": 238,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { swhdb } from '@main/data'\nimport { IHostsListObject } "
  },
  {
    "path": "src/main/actions/list/moveItemToTrashcan.ts",
    "chars": 959,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { getList } from '@main/actions'\nimport { broadcast } fr"
  },
  {
    "path": "src/main/actions/list/moveManyToTrashcan.ts",
    "chars": 212,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { moveToTrashcan } from '@main/actions'\n\nexport default "
  },
  {
    "path": "src/main/actions/list/setList.ts",
    "chars": 230,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { swhdb } from '@main/data'\nimport { IHostsListObject } "
  },
  {
    "path": "src/main/actions/migrate/checkIfMigration.ts",
    "chars": 576,
    "preview": "/**\n * checkIfMigration\n * check if migration is required\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport ge"
  },
  {
    "path": "src/main/actions/migrate/export.ts",
    "chars": 937,
    "preview": "/**\n * export\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport getI18N from '@main/core/getI18N'\nimport { swh"
  },
  {
    "path": "src/main/actions/migrate/import.ts",
    "chars": 1433,
    "preview": "/**\n * import\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport importV3Data from '@main/actions/migrate/impor"
  },
  {
    "path": "src/main/actions/migrate/importFromUrl.ts",
    "chars": 1256,
    "preview": "/**\n * importFromUrl\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport importV3Data from '@main/actions/migrat"
  },
  {
    "path": "src/main/actions/migrate/importV3Data.ts",
    "chars": 737,
    "preview": "/**\n * importV3Data\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\n// import data from v3 to v4\n\nimport { swhdb } "
  },
  {
    "path": "src/main/actions/migrate/migrateData.ts",
    "chars": 1020,
    "preview": "/**\n * migrateData\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\n// migrate data from v3 to v4\n\nimport importV3Da"
  },
  {
    "path": "src/main/actions/openUrl.ts",
    "chars": 159,
    "preview": "/**\n * @author oldj\n * @blog https://oldj.net\n */\n\nimport { shell } from 'electron'\n\nexport default async (url: string) "
  },
  {
    "path": "src/main/actions/ping.ts",
    "chars": 243,
    "preview": "/**\n * ping\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nconst wait = (ms: number) => new Promise((resolve) => s"
  },
  {
    "path": "src/main/actions/quit.ts",
    "chars": 266,
    "preview": "/**\n * quit\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { app } from 'electron'\n\nexport default async ()"
  },
  {
    "path": "src/main/actions/showItemInFolder.ts",
    "chars": 299,
    "preview": "/**\n * showItemInFolder\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { isDir } from '@main/utils/fs2'\nimp"
  },
  {
    "path": "src/main/actions/trashcan/clear.ts",
    "chars": 544,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { swhdb } from '@main/data'\nimport { flatten } from '@co"
  },
  {
    "path": "src/main/actions/trashcan/deleteItem.ts",
    "chars": 816,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { swhdb } from '@main/data'\nimport { ITrashcanListObject"
  },
  {
    "path": "src/main/actions/trashcan/getList.ts",
    "chars": 242,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { swhdb } from '@main/data'\nimport { IHostsListObject } "
  },
  {
    "path": "src/main/actions/trashcan/restoreItem.ts",
    "chars": 1158,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { getList, setList } from '@main/actions'\nimport { swhdb"
  },
  {
    "path": "src/main/actions/updateTrayTitle.ts",
    "chars": 728,
    "preview": "/**\n * toggleTrayTitle\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { getList } from '@main/actions/index"
  },
  {
    "path": "src/main/core/agent.ts",
    "chars": 219,
    "preview": "/**\n * agent\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { ipcMain } from 'electron'\n\nexport const broad"
  },
  {
    "path": "src/main/core/getI18N.ts",
    "chars": 341,
    "preview": "/**\n * getLang\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { configGet } from '@main/actions'\nimport { L"
  },
  {
    "path": "src/main/core/message.ts",
    "chars": 2283,
    "preview": "/**\n * message\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport * as actions from '@main/actions'\nimport { Ac"
  },
  {
    "path": "src/main/core/popupMenu.ts",
    "chars": 729,
    "preview": "/**\n * contextMenu\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { broadcast } from '@main/core/agent'\nimp"
  },
  {
    "path": "src/main/core/updater.ts",
    "chars": 3807,
    "preview": "import events from '@common/events'\nimport { AppDownloadedUpdateInfo, AppUpdateInfo, AppUpdateProgress } from '@common/u"
  },
  {
    "path": "src/main/data/index.ts",
    "chars": 1095,
    "preview": "/**\n * db\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport * as path from 'path'\nimport PotDb from 'potdb'\nim"
  },
  {
    "path": "src/main/http/api/index.ts",
    "chars": 248,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { Hono } from 'hono'\nimport list from './list'\nimport to"
  },
  {
    "path": "src/main/http/api/list.ts",
    "chars": 596,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { getList } from '@main/actions'\nimport { IHostsListObje"
  },
  {
    "path": "src/main/http/api/toggle.ts",
    "chars": 629,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { getList } from '@main/actions'\nimport { broadcast } fr"
  },
  {
    "path": "src/main/http/index.ts",
    "chars": 1565,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { http_api_port } from '@common/constants'\nimport { serv"
  },
  {
    "path": "src/main/libs/cron.ts",
    "chars": 1770,
    "preview": "/**\n * cron\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { checkUpdate, configGet, getList, refreshHosts "
  },
  {
    "path": "src/main/libs/getConfigDir.ts",
    "chars": 269,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport * as path from 'path'\nimport { homedir } from 'os'\n\nexpo"
  },
  {
    "path": "src/main/libs/getDataDir.ts",
    "chars": 358,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport * as path from 'path'\nimport { homedir } from 'os'\n\nexpo"
  },
  {
    "path": "src/main/libs/getIndex.ts",
    "chars": 425,
    "preview": "/**\n * getIndex\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport isDev from '@main/libs/isDev'\nimport path fr"
  },
  {
    "path": "src/main/libs/isDev.ts",
    "chars": 139,
    "preview": "/**\n * isDev\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nexport default () => {\n  return process.env.NODE_ENV ="
  },
  {
    "path": "src/main/libs/request.ts",
    "chars": 1248,
    "preview": "/**\n * request\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { configGet } from '@main/actions'\nimport axi"
  },
  {
    "path": "src/main/libs/safePSWD.ts",
    "chars": 224,
    "preview": "/**\n * safe-pswd\n * @author oldj\n * @blog https://oldj.net\n */\n\nexport default (pswd: string): string => {\n  return (\n  "
  },
  {
    "path": "src/main/libs/tracer.ts",
    "chars": 750,
    "preview": "import { configGet } from '@main/actions'\nimport { GET } from '@main/libs/request'\nimport { server_url } from '@common/c"
  },
  {
    "path": "src/main/main.ts",
    "chars": 4378,
    "preview": "/**\n * main.ts\n * @author oldj\n * @homepage https://oldj.net\n */\n\nimport { configAll, configGet } from '@main/actions'\ni"
  },
  {
    "path": "src/main/preload.ts",
    "chars": 2020,
    "preview": "/**\n * preload\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { Actions } from '@common/types'\nimport { IPo"
  },
  {
    "path": "src/main/types.d.ts",
    "chars": 895,
    "preview": "/**\n * index\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport Tracer from '@main/libs/tracer'\nimport { Locale"
  },
  {
    "path": "src/main/ui/checkSystemLocale.ts",
    "chars": 879,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { languages, LocaleName } from '@common/i18n'\nimport { a"
  },
  {
    "path": "src/main/ui/find.ts",
    "chars": 1523,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { broadcast } from '@main/core/agent'\nimport getIndex fr"
  },
  {
    "path": "src/main/ui/menu.ts",
    "chars": 6962,
    "preview": "/**\n * @author oldj\n * @blog https://oldj.net\n */\n\nimport { findShow } from '@main/actions'\nimport events from '@common/"
  },
  {
    "path": "src/main/ui/tray/index.ts",
    "chars": 5910,
    "preview": "/**\n * tray\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { configGet, configSet, updateTrayTitle } from '"
  },
  {
    "path": "src/main/ui/tray/window.ts",
    "chars": 1597,
    "preview": "/**\n * window\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport getIndex from '@main/libs/getIndex'\nimport isD"
  },
  {
    "path": "src/main/utils/fs2.ts",
    "chars": 219,
    "preview": "/**\n * fs2\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport * as fs from 'fs'\n\nexport const isDir = (dir_path"
  },
  {
    "path": "src/renderer/common/PageWrapper.tsx",
    "chars": 362,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport Loading from '@renderer/components/Loading'\nimport React"
  },
  {
    "path": "src/renderer/components/About/AboutContent.module.scss",
    "chars": 629,
    "preview": "@use '../../styles/common';\n\n.root {\n  // padding-bottom: 20px;\n\n  a {\n    color: var(--swh-primary-color);\n\n    &:hover"
  },
  {
    "path": "src/renderer/components/About/AboutContent.tsx",
    "chars": 1431,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport logo from '@/assets/logo@512w.png'\nimport version from '"
  },
  {
    "path": "src/renderer/components/About/index.module.scss",
    "chars": 97,
    "preview": ".close_btn {\n  position: absolute;\n  right: 12px;\n  top: 12px;\n}\n\n.body {\n  padding-top: 10px;\n}\n"
  },
  {
    "path": "src/renderer/components/About/index.tsx",
    "chars": 765,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport events from '@common/events'\nimport { Modal } from '@man"
  },
  {
    "path": "src/renderer/components/BrowserLink.tsx",
    "chars": 630,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport events from '@common/events'\nimport { actions, agent } f"
  },
  {
    "path": "src/renderer/components/EditHostsInfo.module.scss",
    "chars": 168,
    "preview": ".ln {\n  margin-bottom: 20px;\n\n  &:last-child {\n    margin-bottom: 0;\n  }\n}\n\n.refresh_info {\n  color: var(--swh-font-colo"
  },
  {
    "path": "src/renderer/components/EditHostsInfo.tsx",
    "chars": 9882,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { FolderModeType, HostsType, IHostsListObject } from '@c"
  },
  {
    "path": "src/renderer/components/Editor/HostsEditor.module.scss",
    "chars": 1893,
    "preview": "@use '../../styles/common';\n\n.root {\n  @include common.code;\n  width: 100%;\n  height: 100%;\n}\n\n.editor {\n  height: calc("
  },
  {
    "path": "src/renderer/components/Editor/HostsEditor.tsx",
    "chars": 10615,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { IHostsListObject } from '@common/data'\nimport events f"
  },
  {
    "path": "src/renderer/components/Editor/hosts_highlight.test.ts",
    "chars": 3381,
    "preview": "/**\n * Tests for hosts file syntax highlighting and comment toggling.\n * Covers HTML rendering of comment / IP / error l"
  },
  {
    "path": "src/renderer/components/Editor/hosts_highlight.ts",
    "chars": 8089,
    "preview": "/**\n * Hosts file syntax highlighting and comment toggling for CodeJar.\n *\n * Highlighting: converts plain-text hosts co"
  },
  {
    "path": "src/renderer/components/History.module.scss",
    "chars": 57,
    "preview": ".selected {\n  background: var(--swh-tree-selected-bg);\n}\n"
  },
  {
    "path": "src/renderer/components/History.tsx",
    "chars": 6259,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { IHostsHistoryObject } from '@common/data'\nimport event"
  },
  {
    "path": "src/renderer/components/HostsViewer.module.scss",
    "chars": 411,
    "preview": "@use \"../styles/common\";\n\n.root {\n  @include common.code;\n  height: 100%;\n}\n\n.content {\n  height: calc(100% - var(--swh-"
  },
  {
    "path": "src/renderer/components/HostsViewer.tsx",
    "chars": 723,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport StatusBar from '@renderer/components/StatusBar'\nimport s"
  },
  {
    "path": "src/renderer/components/ItemIcon.tsx",
    "chars": 854,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport {\n  IconDeviceDesktop,\n  IconFileText,\n  IconFolder,\n  I"
  },
  {
    "path": "src/renderer/components/Lang.tsx",
    "chars": 478,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { LocaleName } from '@common/i18n'\nimport useI18n from '"
  },
  {
    "path": "src/renderer/components/LeftPanel/SystemHostsItem.module.scss",
    "chars": 315,
    "preview": ".root {\n  height: var(--swh-tree-row-height);\n  line-height: var(--swh-tree-row-height);\n  border-radius: var(--swh-bord"
  },
  {
    "path": "src/renderer/components/LeftPanel/SystemHostsItem.tsx",
    "chars": 803,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport ItemIcon from '@renderer/components/ItemIcon'\nimport use"
  },
  {
    "path": "src/renderer/components/LeftPanel/Trashcan.module.scss",
    "chars": 31,
    "preview": ".root {\n  user-select: none;\n}\n"
  },
  {
    "path": "src/renderer/components/LeftPanel/Trashcan.tsx",
    "chars": 2465,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { ITrashcanListObject } from '@common/data'\nimport Trash"
  },
  {
    "path": "src/renderer/components/LeftPanel/TrashcanItem.module.scss",
    "chars": 294,
    "preview": ".root {\n}\n\n.count {\n  color: var(--swh-font-color-weak);\n  margin-left: 1em;\n  font-size: 90%;\n}\n\n.trashcan_title {\n  op"
  },
  {
    "path": "src/renderer/components/LeftPanel/TrashcanItem.tsx",
    "chars": 2349,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { ITrashcanListObject } from '@common/data'\nimport ItemI"
  },
  {
    "path": "src/renderer/components/LeftPanel/index.module.scss",
    "chars": 231,
    "preview": "@use \"../../styles/common\";\n\n.list {\n  position: relative;\n  height: calc(100vh - var(--swh-top-bar-height));\n  overflow"
  },
  {
    "path": "src/renderer/components/LeftPanel/index.tsx",
    "chars": 924,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport events from '@common/events'\nimport Trashcan from '@rend"
  },
  {
    "path": "src/renderer/components/List/ListItem.module.scss",
    "chars": 1022,
    "preview": ".root {\n  display: flex;\n\n  &.selected:not(.is_tray):hover {\n    .edit {\n      display: flex;\n    }\n  }\n\n  .edit {\n    d"
  },
  {
    "path": "src/renderer/components/List/ListItem.tsx",
    "chars": 5673,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { IHostsListObject } from '@common/data'\nimport events f"
  },
  {
    "path": "src/renderer/components/List/index.module.scss",
    "chars": 928,
    "preview": ".root {\n\n}\n\n.node {\n  position: relative;\n  line-height: var(--swh-tree-row-height);\n  border-radius: 4px;\n\n  &:hover {\n"
  },
  {
    "path": "src/renderer/components/List/index.tsx",
    "chars": 7668,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { IHostsListObject } from '@common/data'\nimport events f"
  },
  {
    "path": "src/renderer/components/Loading.module.scss",
    "chars": 32,
    "preview": ".root {\n  padding: 40px 20px;\n}\n"
  },
  {
    "path": "src/renderer/components/Loading.tsx",
    "chars": 292,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport useI18n from '@renderer/models/useI18n'\nimport styles fr"
  },
  {
    "path": "src/renderer/components/MainPanel/index.module.scss",
    "chars": 130,
    "preview": ".root {\n  width: 100%;\n  height: calc(100vh - var(--swh-top-bar-height));\n  //overflow: auto;\n  background: var(--swh-ma"
  },
  {
    "path": "src/renderer/components/MainPanel/index.tsx",
    "chars": 570,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport events from '@common/events'\nimport HostsEditor from '@r"
  },
  {
    "path": "src/renderer/components/Pref/Advanced.tsx",
    "chars": 3274,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { ConfigsType } from '@common/default_configs'\nimport { "
  },
  {
    "path": "src/renderer/components/Pref/Commands.tsx",
    "chars": 1367,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { ConfigsType } from '@common/default_configs'\nimport { "
  },
  {
    "path": "src/renderer/components/Pref/CommandsHistory.tsx",
    "chars": 2746,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { ICommandRunResult } from '@common/data'\nimport { Actio"
  },
  {
    "path": "src/renderer/components/Pref/General.tsx",
    "chars": 6447,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { http_api_port } from '@common/constants'\nimport { Conf"
  },
  {
    "path": "src/renderer/components/Pref/Proxy.tsx",
    "chars": 2193,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { ConfigsType, ProtocolType } from '@common/default_conf"
  },
  {
    "path": "src/renderer/components/Pref/index.tsx",
    "chars": 4190,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { ConfigsType } from '@common/default_configs'\nimport ev"
  },
  {
    "path": "src/renderer/components/Pref/styles.module.scss",
    "chars": 405,
    "preview": ".link {\n  text-decoration: underline;\n  color: inherit;\n}\n\n.tabs {\n  display: flex;\n  flex: 1;\n  flex-direction: column;"
  },
  {
    "path": "src/renderer/components/SetWriteMode.module.scss",
    "chars": 47,
    "preview": ".root {\n}\n\n.label {\n  margin: 10px 0 20px 0;\n}\n"
  },
  {
    "path": "src/renderer/components/SetWriteMode.tsx",
    "chars": 2791,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { WriteModeType } from '@common/default_configs'\nimport "
  },
  {
    "path": "src/renderer/components/SideDrawer.tsx",
    "chars": 1294,
    "preview": "import type { DrawerProps } from '@mantine/core'\nimport { Box, Drawer } from '@mantine/core'\nimport type { CSSProperties"
  },
  {
    "path": "src/renderer/components/StatusBar.module.scss",
    "chars": 275,
    "preview": ".root {\n  color: var(--swh-status-bar-font-color);\n  height: var(--swh-status-bar-height);\n  line-height: var(--swh-stat"
  },
  {
    "path": "src/renderer/components/StatusBar.tsx",
    "chars": 916,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { Box, Flex, Group } from '@mantine/core'\nimport prettyB"
  },
  {
    "path": "src/renderer/components/SudoPasswordInput.module.scss",
    "chars": 47,
    "preview": ".root {\n}\n\n.label {\n  margin: 10px 0 20px 0;\n}\n"
  },
  {
    "path": "src/renderer/components/SudoPasswordInput.tsx",
    "chars": 2053,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { IHostsListObject } from '@common/data'\nimport events f"
  },
  {
    "path": "src/renderer/components/SwitchButton.module.scss",
    "chars": 1070,
    "preview": "@use 'sass:math';\n\n$swh-btn-width: 1.6em;\n$swh-btn-height: 0.9em;\n\n.root {\n  position: relative;\n  display: inline-block"
  },
  {
    "path": "src/renderer/components/SwitchButton.tsx",
    "chars": 943,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport clsx from 'clsx'\nimport { useEffect, useState } from 're"
  },
  {
    "path": "src/renderer/components/TopBar/ConfigMenu.module.scss",
    "chars": 112,
    "preview": ".menu_list {\n  max-height: calc(100vh - 80px);\n  overflow-y: auto;\n\n  button > span {\n    font-size: 1em;\n  }\n}\n"
  },
  {
    "path": "src/renderer/components/TopBar/ConfigMenu.tsx",
    "chars": 4865,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { feedback_url, homepage_url } from '@common/constants'\n"
  },
  {
    "path": "src/renderer/components/TopBar/ImportFromUrl.module.scss",
    "chars": 47,
    "preview": ".root {\n}\n\n.label {\n  margin: 10px 0 20px 0;\n}\n"
  },
  {
    "path": "src/renderer/components/TopBar/ImportFromUrl.tsx",
    "chars": 2569,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { Button, Group, Modal, TextInput } from '@mantine/core'"
  },
  {
    "path": "src/renderer/components/TopBar/index.module.scss",
    "chars": 901,
    "preview": "@use '../../styles/common';\n\n.root {\n  $w: 150px;\n  $p: 10px;\n\n  background: var(--swh-top-bar-bg);\n  border-bottom: 1px"
  },
  {
    "path": "src/renderer/components/TopBar/index.tsx",
    "chars": 4376,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport events from '@common/events'\nimport { ActionIcon, Box, F"
  },
  {
    "path": "src/renderer/components/Transfer.module.scss",
    "chars": 301,
    "preview": ".root {\n\n}\n\n.title {\n  span {\n    font-size: 80%;\n    color: var(--swh-font-color-weak);\n  }\n}\n\n.list {\n  height: 200px;"
  },
  {
    "path": "src/renderer/components/Transfer.tsx",
    "chars": 4849,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { ActionIcon, Box, Center, Stack } from '@mantine/core'\n"
  },
  {
    "path": "src/renderer/components/Tree/Node.tsx",
    "chars": 9061,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { ITreeNodeData, NodeIdType } from '@common/tree'\nimport"
  },
  {
    "path": "src/renderer/components/Tree/Tree.tsx",
    "chars": 5554,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { ITreeNodeData, NodeIdType } from '@common/tree'\nimport"
  },
  {
    "path": "src/renderer/components/Tree/fn.ts",
    "chars": 6146,
    "preview": "import { ITreeNodeData, NodeIdType } from '@common/tree'\nimport lodash from 'lodash'\nimport { DropWhereType } from './Tr"
  },
  {
    "path": "src/renderer/components/Tree/index.tsx",
    "chars": 230,
    "preview": "/**\n * @author: oldj\n * @homepage: https://oldj.net\n */\n\nimport { KeyMapType as _KeyMapType } from './fn'\n\nexport { objK"
  },
  {
    "path": "src/renderer/components/Tree/style.module.scss",
    "chars": 1716,
    "preview": ".root {\n  $select-bg-color: var(--tree-drag-select-bg-color);\n  $drop-indicator-color: var(--tree-drag-indicator-color);"
  }
]

// ... and 48 more files (download for full content)

About this extraction

This page contains the full source code of the oldj/SwitchHosts GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 248 files (468.2 KB), approximately 138.3k tokens, and a symbol index with 325 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!