野良 Scrapbox アプリの fav 機能

Scrapbox の野良 Electron アプリにちょっとしたヒストリとブックマーク機能を追加しようと思いました。

最初はメニューに履歴を追加してピン止めするような UI を作ろうとしましたが、Electron では動的にメニューアイテムを追加削除する API が提供されてませんでした。

そこでツールバーに select-box を置いて動的に option 要素として追加するようにしてみました。

履歴とピン止めを同じリストで扱う UI の実装が複雑になるので、とりあえずピン止め用のリストを切り出しました。

そのうち履歴要らないって気持ちになってきました。Scrapbox 自体が履歴表示しますし、自前のページリスト画面もあるし。

ということで、fav 機能だけに絞りました。

着け外しの UI も実装、操作ともに面倒だし、fav リストに入れたページは最上位に出して、一定数の上限値を超えたものは古い方から消していくキューのような感じで、キューから消えちゃったものはまた検索して出せばよいと。

f:id:kondoumh:20190227055053p:plain

結果、ヒストリ的なピン止め機能といった UI に落ち着きました。

f:id:kondoumh:20190228055504g:plain

Release v0.6.0 · kondoumh/sbe · GitHub

Vue を使って Electron アプリを開発する

blog.kondoumh.com

Vue のことがちょっと分かった気になれました。すると、最近よく触っている Electron でのアプリ開発にも取り込みたくなります。

github.com

Scrapbox in Electron は WebView でサイトの画面を表示していますが、Electron では当然自前で画面を作ることもできます。その場合は Vue のようなフレームワークを適用するのが自然でしょう。

  • Web とアセットを共有したい
  • フレームワークのツールチェインや知識を流用したい

という意図があります。

electron-vue という Vue と Electron を使ったプロジェクトのボイラープレートがあります。

f:id:kondoumh:20190224130025p:plain

github.com

vue-cli が導入されていれば、以下のコマンドを叩いて、プロジェクト名などを入力するだけ。

$ vue init simulatedgreg/electron-vue my-project

Electron のインストールもお任せできます。

生成されたプロジェクトディレクトリに移動して

$ npm run dev

で、デフォルトの Electron アプリが起動します。

f:id:kondoumh:20190224125417p:plain

Vue 用のフォルダは src/renderer/components 配下になりますので、ここに Web アプリ用の Vue ファイルなどを配置します。

f:id:kondoumh:20190224140252p:plain

動きました。

f:id:kondoumh:20190224133624p:plain

Electron アプリとして作りこむためには、Renderer と Vue コンポーネント間の連携が必要ですが、そこをどう作りこむのか、Web とアセットの共有がどこまで可能かについてはやってみないと分からないところです。

2019/2/27 追記

Main Process のメニューから Renderer Process 経由で、ダイアグラムにノードを追加するコードはこんな感じになりました。

src/main/index.js

function createMenu () {
  console.log('createMenu')
  const template = [
    {
      label: 'Edit',
      submenu: [
        {
          label: 'add',
          click () {
            mainWindow.webContents.send('add', 'hoge')
          }
        }
      ]
    }
  ]

  const menu = Menu.buildFromTemplate(template)
  Menu.setApplicationMenu(menu)
}

src/renderer/components/JointDia.vue

<script>
  import joint from 'jointjs'

  export default {
    name: 'JointDia',
    mounted () {
      this.graph = new joint.dia.Graph()
         :
      this.$electron.ipcRenderer.on('add', (e, data) => {
        this.addNode(data)
      })
    },
</script>

this.$electron.ipcRenderer で IPC イベントを処理できます。

この例では、イベントを受けたいコンポーネントの mounted に直接ハンドラーを書きましたが、App.vue からディスパッチするようにすれば、サブコンポーネントは Web と同一にできそうな気がしました。

Vue と JointJS でトイ・ダイアグラムエディタ

Vue 人気ありますね。あまり真剣に触ったことがなく ToDo サンプルとかだとどうも分かった気になれない。ということで JointJS 使って CodePen で書いたトイ・ダイアグラムエディタを題材に構造を検討してみます。

blog.kondoumh.com

JointJS は Backbone ベースなので Vue にうまく混ぜられるのかなあと思いましたが、あっさり表示できました。data に graph オブジェクトを保持して mounted で joint.dia.Graph オブジェクトを設定すれば、methods 内のメソッドから graph オブジェクト経由でダイアグラムにノードを追加することができるようになりました。

JointDia.vue 抜粋

<template>
  <div>
    <div ref="myholder"></div>
    <input type="text" placeholder="new node name" v-model="nodeName" />
    <input type="button" v-on:click="addNode" value="add node" />
  </div>
</template>

<script>
  export default {
    mounted() {
      this.graph = new joint.dia.Graph
      let paper = new joint.dia.Paper({
        el: this.$refs.myholder,
        model: this.graph,
      }
    },
    data() {
      return {
        graph: {}
      }
    },
    methods : {
      addNode() {
        const rect = new joint.shapes.standard.Rectangle()
        rect.addTo(this.graph)
      }
    }
</script>

一通り動きましたが、ダイアグラム部分とノード登録の Form 部分が分かれていないので両者のデータ、メソッドが混然一体となっています。Vue らしく別コンポーネントにしたいところです。

Form 部分を InputForm.Vue に切り出しました。追加するノード名を nodeName というデータで保持し、ボタンのクリック時 $emit で親コンポーネント (App.vue) に通知するようにしました。

<template>
  <div>
    <div>
      <input type="text" placeholder="new node name" v-model.trim="nodeName" />
      <input type="button" v-on:click="add" value="add node" />
    </div>
  </div>
</template>

<script>
  export default {
    name: 'InputForm',
    data() {
      return {
          nodeName: '',
      }
    },
    methods: {
      add() {
        this.$emit('addNode', this.nodeName)
        this.nodeName = ''
      }
    }
  }
</script>

App.vue では InputForm と JointDia をコンポーネントとして利用しています。InputForm の v-on 属性で InputForm で emit したイベントハンドラーの関数 addNode を登録しています JointDia の v-bind: 属性で JointDia の nodeName プロパティと App.vue の data である nodeName をバインドしています。addNode 関数では、nodeName 受け取ったデータで書き換えます。これにより、JointDia コンポーネントの nodeName プロパティが書き換わります。

<template>
  <div id="app">
    <InputForm v-on:addNode="addNode" />
    <JointDia v-bind:nodeName="nodeName" />
  </div>
</template>

<script>
import JointDia from '@/components/JointDia'
import InputForm from '@/components/InputForm'

export default {
  name: 'App',
  components: {
    JointDia,
    InputForm
  },
  data() {
    return {
      nodeName: ''
    }
  },
  methods: {
    addNode(name) {
      this.nodeName = name
    }
  }
}
</script>

JointDia.vue の抜粋。プロパティ nodeName の変化を watch し変化時に Graph にノードを追加しています。

<template>
  <div>
    <div ref="myholder"></div>
  </div>
</template>

<script>
  import joint from 'jointjs'

  export default {
    name: 'JointDia',
    mounted(){
    },
    data() {
    },
    props: {
      nodeName: String,
    },
    watch: {
      nodeName: {
        handler (newVal, oldVal) {
          this.addNode(newVal)
        }
      }
    },
    methods: {
      addNode(name) {
      }
    }
  }
</script>

コンポーネントを分割すると、データ伝搬のコードが増えますが、コンポーネント内部は自身の責務に集中できてシンプルになる感じです。Vue らしいもっとすっきりした書き方があるかもしれませんが、ひとまずコードは CodeSandbox に置きました。

codesandbox.io

新規追加のノード名だけではなくダイアグラム内のノードのコレクションも管理する場合、JointJS のデータ構造とマッピングするためのコードを追加していく必要があります。

個人 Scrapbox を1プロジェクトに統合しました。

Scrapbox を使い始めた時テーマ別にプロジェクトを分けました。

blog.kondoumh.com

最近会社 Scrapbox で日々ページが増殖し、ページのグラフが生成されていく様子を見ていて、個人の限られた情報を分割しているのはイマイチと思うようになりました。色々な情報が混在した Scrapbox はブログや Wiki とは違うダイナミックな感じがあります。

プロジェクトの切り替えやプロジェクト跨りのリンクを貼るのが面倒というのもあります。

Scrapbox の最近の入門記事でも解説されています。

rashita.net

で、そのページをまとめるのが「プロジェクト」という概念です。これは、他のツールの「ノートブック」に相当するもの、というよりもむしろ「アカウント」くらいの感覚が近しいでしょう。

言い換えれば、自分ひとりが扱う情報なら、雑多に混ぜて一つの「プロジェクト」にまとめた方がいい、ということです。あるいは、公開用と非公開用で切り分けるくらいでしょうか。

やはりそうか、ということで分断されたプロジェクトを統合することにしました。

作業としては、以下のようにしました。エクスポート、インポートがよくできているのでたいして時間はかかりませんでした。

  1. 移行先のプライベートプロジェクトを作る
  2. 移行元の各プロジェクトからデータをエクスポートする(Settings -> Page Data -> Export Pages)
  3. 移行先プロジェクトでデータをインポートする (Settings -> Page Data -> Import Pages)
  4. インポート後、リンクを整えたり、ページを選んで Pin したりする
  5. 移行先プロジェクトを公開する
  6. ブログやホームページに貼ったリンクを移行先のものに変更する
  7. 移行元のプロジェクトを削除する

統合したプロジェクトです。

scrapbox.io

Scrapbox の開発者橋本さんがこのようなページを書かれていました。

scrapbox.io

確かにタグにせず文章の中に埋め込む方が、グラフ構造が自己組織的に生成される感じでよいとは思うのですが、インデックス用のページを作ったりするのにタグは便利で使ってしまっています。

大きく3つのインデックスページを作って Pin しています。

scrapbox.io

scrapbox.io

scrapbox.io

これまでプライベートにしていた個人開発のページも混ぜました。コードを書き始める前のアイデアの整理や関連情報の収集といった作業に使えそうです。

入り口は分けていますが、中のページ感はいい感じで繋がっていきそうな感触があります。

Rust 環境構築 - Emacs & VS Code

昨年末から少しずつ Rust に入門してます。日本語版チュートリアルが読みやすくてありがたいです。

doc.rust-jp.rs

メモリ安全、マルチパラダイム、システムプログラミング言語で WebAssembly にも対応とか色々特徴はありますが、C++ に馴染んでいると入りやすい気がします。まだ使い途は見えてませんが、新しい言語を学ぶのは興味深いものです。

EC2 インスタンス(Ubuntu 18.04.1) で Emacs の Rust 環境を構築してみました。

Rust のインストールは公式サイト通り

www.rust-lang.org

$ curl https://sh.rustup.rs -sSf | sh

で導入して、PATH の設定をします。

Ubuntu に Emacs をインストールするとちょっと古い Emacs 25 が入ってしまうのでリポジトリを追加して Emacs 26 を導入。

$ sudo add-apt-repository ppa:kelleyk/emacs
$ sudo apt install emacs26

最近 VS Code ばかり使っていて Emacs 知識をアップデートしてませんでしたが、久々に設定を見直しました。require によるパッケージロードを use-package に移行。これにより、パッケージが導入されていない場合 Emacs の起動処理がエラー中断されることなく継続されます。複数環境で Emacs 設定を共有したい場合にこの振る舞いが役立ちます。

www.emacswiki.org

コード補完はずっと auto-complete 使ってましたが、開発が活発な company にスイッチ。UI はちょっと IDE っぽい見た目です。

company-mode.github.io

RLS (Rust Language Server) を使える rust-mode の fork である rustic を使おうとしたのですが、自分の環境ではうまく動作せず。rust-mode と racer を使いました。

$  cargo install --version 2.0.13 racer

racer のバージョンを指定しないとビルドエラーになりました。

rust-src を追加します。

$ rustup component add rust-src

Emacs では、rust-mode, racer を package-install して、init.el に設定を追加

;; rust-mode
(use-package rust-mode)
(add-to-list 'exec-path(expand-file-name "~/.cargo/bin/"))
(add-to-list 'auto-mode-alist '("\\.rs\\'"  . rust-mode))
(add-hook 'rust-mode-hook #'racer-mode)
(add-hook 'racer-mode-hook #'eldoc-mode)

コード補完などが効くようになりました。

VS Code では、Rust(rls) 拡張をインストールするだけで、rls のインストールも自動で環境を整えてくれます。とても楽です。

marketplace.visualstudio.com

EC2 インスタンスを Android / iOS から起動・接続・停止する環境を構築する

これまで VPS をレンタルしてましたが、利用頻度がさほどでもないので、必要時だけ EC2 のインスタンスを起動することで定額課金を無くそうと思いました。t2.micro や t2.nano のインスタンスは開発環境としてはそこそこのスペックで時間当たり料金も安いので財布にやさしいのです。

モバイルから Web UI でインスタンスの起動や停止を行うのは面倒なので、AWS CLI を使いたいところです。AWS CLI の実行には Python が必要なので単なる SSH クライアントアプリだと動きません。

ということで、AWS のドキュメントを参考にしながら Android と iOS でそれぞれ環境構築してみました。

docs.aws.amazon.com

Android

もちろん Termux を使います。

play.google.com

Python 環境を構築して、awscli を pip install します。

# Python パッケージをインストール
$ pkg install python python-dev

# venv を作成
$ python3 -m venv enva

# venv をアクティベート
$ source /enva/bin/activate
(enva) $ 

# pip で awscli をインストール
(enva) $ pip install awscli 

# awscli 起動確認
(enva) $ aws --version
aws-cli/1.16.96 Python/3.7.2 Linux/4.9.96-gce70628-ab5122554 botocore/1.12.86

iOS

iSH (ベータテスト中) を使いました。

github.com

iSH は Alpine Linux ベースなので apk コマンドでパッケージをインストールします。

# sudo をインストール sudo ユーザ追加
# apk add sudo
# adduser user1
# vi /etc/sudoers

# python3 をインストール
$ sudo apk add python3 python3-dev

# venv を作成
$ python3 -m venv enva

# venv をアクティベート
$ source enva/bin/activate

# awscli  を pip でインストール
(enva) $ pip3 install awscli

# awscli 起動確認
(enva) $ aws --version
aws-cli/1.16.96 Python/3.6.6 Linux/3.2.0-ish botocore/1.12.86

iSH は Python のチューニングがまだみたいでけっこう待たされますが一応 aws コマンドは使えるようになりました。

EC2 インスタンスの作成

AWS の EC2 マネージメントコンソールでキーペアを作成し、作成したキーペアを使って EC2 インスタンスを AMI から作成します。 今回は、Ubuntu を選択しました。

キーペアの作成も EC2 マネージメントコンソールで行いました。AWS CLI でコマンドを叩く方法もあるのですが、インスタンスを複数の端末から使うため、キーペアのファイルを取得しておきたいためです。

AWS CLI の設定

あとは端末ごとに設定を行います。AWS CLI の設定用サブコマンドでアクセスキーID、アクセスキー、リージョン、出力フォーマットを入力します。

(enva) $ aws configure
AWS Access Key ID [None]: xxxxxxxxxxxx
AWS Secret Access Key [None]:  xxxxxxxxxxxxxxxxxxxxxxxxx
Default region name [None]: ap-northeast-1
Default output format [None]: text

キーペアのファイルを保存し、読み取り権限を変更しておきます。

$ chmod 400 devenv-key.pem 

以上で、EC2 インスタンスへの接続準備ができました。

起動、接続、停止

固定 IP アドレスは取得していないので、起動のたびにインスタンス ID から取得する必要があります。

#  venv をアクティベート
$ source enva/bin/activate

# インスタンス起動
(enva) $ aws ec2 start-instances --instance-ids "i-xxxxxxxxxxxxxxxxxxx"

# IP アドレス取得
(enva) $ aws ec2 describe-instances --instance-ids i-xxxxxxxxxxxxxxxxxxx --query "Reservations[0].Instances[0].PublicIpAddress"

# ssh でログイン
(enva) $ ssh -i devenv-key.pem ubuntu@xx.xxx.xxx.xxx

# インスタンス停止
(enva) $ aws ec2 stop-instances --instance-ids "i-xxxxxxxxxxxxxxxxxxx"

シェルにしておいた方がいいですね。これで Pixel 3 からも iPad からも EC2 インスタンスを起動して SSH 接続、停止ができるようになりました。iSH はまだベータ版とはいえ、SSH に関しては問題なく作業できる状態です。

EC2 インスタンス自体も必要時に AMI から作って環境構築を Ansible で実行するよう自動化しておけば、Immutable Infrastructure をさらに進めることができます。今後 Ansible Playbook を整備していきたいと思います。

Scrapbox Electron アプリ - タイトル付き書式でリンクを貼る機能

Scrapbox には URL をペーストすると、その Web ページのタイトルを取得して、[url title] の記法でペーストしてくれる Chrome 拡張や Bookmarklet がいくつかあります。

ScrapScripts もそのひとつ。

chrome.google.com

同僚からこの拡張の使い方を教えてもらい、確かにこの機能は取り込むべきだと思いました。

まずは、安直にアプリ起動時に ScrapScripts 拡張をそのままロードする方法を試みました。Electron には BrowserWindow.addExtension という API が生えています。この API を使うと一応ロードはできていますが、肝心のペースト機能は動作しません。

Electron は Chrome 拡張のサポートはしない *1 というスタンスのようです。まあ、ブラウザ用に作られた拡張のためのエミュレートは難しいということだろうと思います。

ということで、同等な機能をアプリ側に実装することにしました。Scrapbox の開発者で ScrapScripts の作者 daiiz さんが実装を解説されています。

scrapbox.io

DOMParser というオブジェクトでページの要素が取り出せるんですね。Electron ではクリップボード周りの API が簡単に使えたり、クロスブラウザの考慮が不要だったりとかなり楽ができます。ページを fetch して DOM を parse するレイテンシーのある非同期処理なので、ステータスバーに経過を表示するようにしてみました。

f:id:kondoumh:20190125223858g:plain

これで、外部リンクを貼るのが楽になりました。

Release v0.3.2 · kondoumh/sbe · GitHub

*1:DevTools 拡張はサポートされています