IT

【Electron】Macで使えるDiffツール作りました。

作成理由

Windowsで開発していた時は、WinMergeを利用していましたがMacで利用できなかったので作りました。

WEBシステムはあまり利用したくなかったのと、VSCodeのDiffが使いづらかったのも一因です。

また、ネイティブアプリにするためにElectronを利用してみました。

ビルドしたアプリを配布するのは面倒なので、コードとコマンド貼っておきます。

動作イメージ

環境セットアップとビルド

1:後述の「コード関連」に記載されている内容を作業ディレクトリに配置

2:パッケージのインストール

npm install

3:アプリのスタート(動作確認用)

npm start

4:アプリケーションのビルド

npm run dist

5:作業ディレクトリ内にdistが作成されて、dmgファイルが生成されます。

生成されるもの:electron-diff-tool-1.0.0-arm64.dmg

6:上記dmgファイルを実行すれば通常のアプリケーションとしてインストールできます。


コード関連

  • ディレクトリ構造
.
├── index.html
├── main.js
├── package.json
├── renderer.js
└── styles.css
  • index.html
<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <title>縁の差分確認ツール</title>
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <div id="app">
    <h1>縁の差分確認ツール</h1>
    <div class="input-container">
      <textarea id="text1" placeholder="ここに元のテキストを入力してください"></textarea>
      <textarea id="text2" placeholder="ここに変更後のテキストを入力してください"></textarea>
    </div>
    <div class="button-container">
      <button id="compareBtn">比較</button>
      <button id="refreshBtn">リフレッシュ</button>
    </div>
    <div id="result">
      <div id="text1Result"></div>
      <div id="text2Result"></div>
    </div>
  </div>
  <script src="renderer.js"></script>
</body>

</html>
  • main.js
const { app, BrowserWindow } = require("electron");
const path = require("path");

function createWindow() {
  const win = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      nodeIntegration: true,
      contextIsolation: false,
    },
  });

  win.loadFile("index.html");
}

app.whenReady().then(createWindow);

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

app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) {
    createWindow();
  }
});
  • renderer.js
const diff = require("diff");

function compareTexts() {
  const original = document.getElementById("text1").value;
  const modified = document.getElementById("text2").value;

  const differences = diff.diffWordsWithSpace(original, modified);
  const originalResult = document.getElementById("text1Result");
  const modifiedResult = document.getElementById("text2Result");

  originalResult.innerHTML = "";
  modifiedResult.innerHTML = "";

  let originalLine = document.createElement("div");
  let modifiedLine = document.createElement("div");

  differences.forEach((part) => {
    const spanOriginal = document.createElement("span");
    const spanModified = document.createElement("span");

    if (part.added) {
      spanModified.textContent = part.value;
      spanModified.className = "added";
      spanOriginal.className = "empty";
    } else if (part.removed) {
      spanOriginal.textContent = part.value;
      spanOriginal.className = "removed";
      spanModified.className = "empty";
    } else {
      spanOriginal.textContent = part.value;
      spanModified.textContent = part.value;
    }

    originalLine.appendChild(spanOriginal);
    modifiedLine.appendChild(spanModified);

    if (part.value.includes("\n")) {
      originalResult.appendChild(originalLine);
      modifiedResult.appendChild(modifiedLine);
      originalLine = document.createElement("div");
      modifiedLine = document.createElement("div");
    }
  });

  if (originalLine.childNodes.length > 0) {
    originalResult.appendChild(originalLine);
    modifiedResult.appendChild(modifiedLine);
  }
}

function refreshAll() {
  document.getElementById("text1").value = "";
  document.getElementById("text2").value = "";
  document.getElementById("text1Result").innerHTML = "";
  document.getElementById("text2Result").innerHTML = "";
}

document.getElementById("compareBtn").addEventListener("click", compareTexts);
document.getElementById("refreshBtn").addEventListener("click", refreshAll);
  • styles.css
:root {
  --primary-color: #3498db;
  --secondary-color: #2ecc71;
  --background-color: #f5f6fa;
  --text-color: #2c3e50;
  --border-color: #dcdde1;
}

body {
  font-family: "Meiryo", "Hiragino Kaku Gothic ProN", "MS PGothic", sans-serif;
  background-color: var(--background-color);
  color: var(--text-color);
  line-height: 1.6;
  padding: 20px;
  margin: 0;
}

#app {
  max-width: 1200px;
  margin: 0 auto;
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  padding: 30px;
}

h1 {
  text-align: center;
  color: var(--primary-color);
  margin-bottom: 30px;
  font-size: 2.5em;
}

.input-container {
  display: flex;
  gap: 20px;
  margin-bottom: 20px;
}

textarea {
  width: 100%;
  height: 200px;
  padding: 15px;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  font-size: 14px;
  resize: vertical;
  transition: border-color 0.3s ease;
}

textarea:focus {
  outline: none;
  border-color: var(--primary-color);
  box-shadow: 0 0 0 2px rgba(52, 152, 219, 0.2);
}

.button-container {
  display: flex;
  justify-content: center;
  gap: 20px;
  margin-bottom: 30px;
}

button {
  padding: 12px 24px;
  font-size: 16px;
  cursor: pointer;
  border: none;
  border-radius: 4px;
  transition: all 0.3s ease;
  font-weight: bold;
}

#compareBtn {
  background-color: var(--primary-color);
  color: white;
}

#compareBtn:hover {
  background-color: #2980b9;
}

#refreshBtn {
  background-color: var(--secondary-color);
  color: white;
}

#refreshBtn:hover {
  background-color: #27ae60;
}

#result {
  display: flex;
  gap: 20px;
}

#text1Result,
#text2Result {
  width: 100%;
  border: 1px solid var(--border-color);
  border-radius: 4px;
  padding: 15px;
  white-space: pre-wrap;
  font-family: "Meiryo", "Hiragino Kaku Gothic ProN", "MS PGothic", monospace;
  line-height: 1.5;
  max-height: 400px;
  overflow-y: auto;
  background-color: #f8f9fa;
}

.added {
  background-color: #e6ffed;
  color: #24292e;
  padding: 2px 0;
}

.removed {
  background-color: #ffeef0;
  color: #24292e;
  padding: 2px 0;
}

.empty {
  display: none;
}

@media (max-width: 768px) {
  .input-container,
  #result {
    flex-direction: column;
  }

  textarea,
  #text1Result,
  #text2Result {
    width: calc(100% - 30px);
  }
}
  • package.json
{
  "name": "electron-diff-tool",
  "version": "1.0.0",
  "description": "simple diff tool",
  "main": "main.js",
  "scripts": {
    "start": "electron .",
    "pack": "electron-builder --dir",
    "dist": "electron-builder"
  },
  "build": {
    "appId": "com.hoge.hageappname",
    "mac": {
      "category": "hoge.app.category.type"
    },
    "win": {
      "target": [
        "nsis",
        "portable"
      ]
    },
    "linux": {
      "target": [
        "AppImage",
        "deb"
      ]
    }
  },
  "dependencies": {
    "diff": "^5.1.0"
  },
  "devDependencies": {
    "electron": "^20.0.0",
    "electron-builder": "^24.13.3"
  }
}

最後に

ライブラリがあったのでサクッと作れました。先人たちには頭が下がりますね。

以上、どなたかのお役に立てば幸いです。

  • この記事を書いた人

緑川縁

ニートからシステムエンジニアになった人
クラウド案件をメインにやっています。
保持資格:CCNA,AWS SAA&SAP,秘書検定2級
趣味でボカロ曲作り始めました。

-IT
-,