WPP
WordPress の “Highlighting Code Block” で対応言語を追加する。

このブログのコードブロックでは “Highlighting Code Block” というプラグインを使っています。

がデフォルトで対応する言語はあまり多くありません。公式の説明では追加方法がわかりにくかったので調べてみまみました。

「ひいらぎナレッジ倉庫」さんのサイトによると新しく対応したい言語を追加したバージョンの Prism.js /Prism.css を追加する必要がありそうです。

ダウンロードとワードプレスへのアップロード

  1. WordPress で [設定] – [[HCB] 設定] に進み、ヘルプの 「こちら」 へリンクに進みます。
  2. Prism のダウンロードページが開くので、必要な言語をチェックしていきます。
  3. ページの下に進み、.js をダウンロードします。(.css は自分の環境では必要なさそうでした。)
  4. [HCB] 設定の [独自 Prism.js] フォルダの指し先に合うようにファイルをアップロードします。
    今回は [FQDN]/[ワードプレストップ]/wp-content/theme/[使用中のテーマ]/prism/prism.js
    になる場所にアップロードしました。
  5. 4. に合わせて [独自 Prism.js] にパスを指定します。
  6. [変更の保存] をクリックします。

対応言語の追加

今回は、XML と Kotlin を追加します。

同じく [HCB] 設定の [使用する言語セット] に以下の行を追加します。もともとの末尾に , がない場合はエラーになるので末尾に追加します。(PHP のハッシュや配列の場合、最後の要素に , が付いていてもエラーにならないので最初からつけておいたほうが簡単です。)

Kotlin: “Kotlin”,
xml: “XML”

これで、 Kotlin と XML を選択してハイライトできるようになった。

参考

Highlighting Code Blockで対応言語を追加する方法 | ひいらぎナレッジ倉庫

Android Kotlin の ViewModel で Model の変更を受け取る -1

今回は珍しく長いです。

先日から Android のタスクにまた取り組んでいます。リハビリをかねて Jetpack compose を勉強しています。

自分の理解では、@composable な関数は外から状態を変更できないので、何らかの方法で外部に状態を維持しなければなりません。

そうなると MVVM って話になるようなのですが、Model と ViewModel の連携についてはあまりいいサンプルが見つけられなかった。ネットに転がっているサンプルでは UI 操作で ViewModel の変更が完結しているものがほとんどです。このパターンでは Model が View をまたぐ場合のことは触れていないので自分で考えなければいけません。r

結局のところ、モデルをビュー(Activity) の外に置くのが一番簡単な解決策じゃないかと思います。

参考リンクが唯一、Model から ViewModel に更新を通知する例を見つけられたのでそれをそのまま写経してみた。

目次

前提

  • Jetpack Compose
  • Kotlin
  • Android Studio
  • Android Studio Koala | 2024.1.1 Patch 1
  • Build #AI-241.18034.62.2411.12071903, built on July 11, 2024

基本のビューモデル

まずは、compose を使って ViewModel の LiveData を UI に通知するパターンをつくる。 ついでに ViewModel を Model の一部とみなして Activity の外で宣言する例。ViewModel は別ファイルに分けるべきですが面倒なのでそのままです。

package xxx.yyy.logpanecompose

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
import red.txn.logpanecompose.ui.theme.LogPaneComposeTheme

class MainActivity : ComponentActivity() {

    val vm = MyApp.getInstance().getViewModel()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()

//        val vm = LogViewModel()

        setContent {
            LogPaneComposeTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    LogPane(vm=vm, modifier = Modifier.padding(innerPadding))
//                    LogPane(modifier=Modifier.padding(innerPadding))
                }
            }
        }
    }
}

class LogViewModel(): ViewModel() {
    val buffer = MutableLiveData("")

    private var count: Int = 0

    fun append(newLine: String) {
        buffer.value = buffer.value + newLine + count.toString() + "\n"
        count++
    }
}

@Composable
fun LogPane(
    modifier: Modifier=Modifier,
    vm: LogViewModel = viewModel()
) {
    val text: String by vm.buffer.observeAsState("")
    Column(modifier=modifier.padding(horizontal=16.dp)) {
        Button(
            onClick={vm.append("aZZZ")},
            modifier=modifier.padding(top=0.dp, bottom=4.dp )
        )
        {
            Text(text="add log")
        }
        TextField(
            value=text,
            onValueChange = { },
            singleLine = false,
            modifier = modifier.fillMaxSize()
        )
    }
}

@Preview(showBackground = true)
@Composable
fun LogPanePreview() {
    LogPaneComposeTheme {
        LogPane()
    }
}

MainActivity の先頭でアプリケーションクラスから ViewModel を持ってきています。

独自のアプリケーションクラスを書きます

package xxx.yyy.logpanecompose

import android.app.Application

class MyApp: Application() {
    public val vm = LogViewModel()  // private にするべきかも

    companion object {
        private var instance: MyApp? = null

        fun getInstance(): MyApp {
            if (instance == null) {
                instance = MyApp()
            }
            return instance!!
        }
    }

    public fun getViewModel() = vm
}

MyApp を使うように AndroidManifest.xml を変更します。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <application
        android:name=".MyApp"
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.LogPaneCompose"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.LogPaneCompose">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

LiveData でモデルの変更を受け取る

Model に LiveData を追加して ViewModel で監視するには下のように Model を追加し、 ViewModel を変更する。

// 省略

data class LogModel(val message: String) {
    val buffer = MutableLiveData(message)

    fun append(newMessage: String) {
        buffer.value += newMessage + "\n"
    }
}

class LogViewModel(val model: LogModel): ViewModel() {
    var buffer = MutableLiveData("")

    private val lifecycleOwner = CustomLifecycleOwner()

    init {
        lifecycleOwner.start()
        model.buffer.observe(lifecycleOwner) {
            println("viewmodel: $it")
            buffer.value = it
        }
    }

    fun append(newMessage: String) {
        model.append(newMessage)
    }

    inner class CustomLifecycleOwner: LifecycleOwner {
        private val registry = LifecycleRegistry(this)

        override val lifecycle: Lifecycle
            get() = registry

        fun start() {
            registry.currentState = Lifecycle.State.STARTED
        }

        @Suppress("unused")
        fun stop() {
            registry.currentState = Lifecycle.State.CREATED
        }
    }
}

MyApp もモデルを宣言するように変更する

package xxx.yyy.logpanecompose

import android.app.Application

class MyApp: Application() {
    val model = LogModel("")
    val vm = LogViewModel(model=model)

    companion object {
        private var instance: MyApp? = null

        fun getInstance(): MyApp {
            if (instance == null) {
                instance = MyApp()
            }
            return instance!!
        }
    }

    fun getViewModel() = vm
}

StateFlow でモデルの変更を受け取る

Model、ViewModel 層で LiveData を使うのはあまりよくないらしいとのことで、StateFlow パターンもやってみる。

// いろいろ省略

data class LogModel(val message: String) {
//    val buffer = MutableLiveData(message)
    val buffer = MutableStateFlow(message)

    fun append(newMessage: String) {
        buffer.value += newMessage + "\n"
    }
}

class LogViewModel(val model: LogModel): ViewModel() {
    var buffer = MutableLiveData("")

    init {
        viewModelScope.launch(Dispatchers.IO) {
            model.buffer.collect {
                println("viewmodel: $it")
                buffer.postValue(it)
            }
        }
    }

    fun append(newMessage: String) {
        model.append(newMessage)
    }
}

参考

僕たちはどのようにしてViewModelに通知すべきなのか #Android - Qiita
Linux Mint 22 で RabbitVCS を使う

さて、今度のプロジェクトでは Subversion (SVN) を使うようなので GUI クライアントの RabbitVCS をインストールしてみた。本体 ? のインストールはパッケージマネージャでインストールできるがそれだけでは、Nemo で TortoiseSVN みたいなことはできない。

パッケージのインストールはこんな感じ

$ sudo apt install rabbitvcs-core

これだけだと Nemo になんの変化もない。 >> つまり使うことができない。

参考リンクの Github のページから RabbitVCS.py をダウンロードして Nemo が認識できるようにする必要がある。

説明に従い /usr/share/nemo-python/extentsions にコピーする

$ sudo cp ./RabbitVCS.py /usr/share/nemo-python/extensions

$ nemo -q
$ pgrep -f service.py | xargs kill
$ nohup nemo > /dev/null &

自分の環境では、該当するプロセスがな存在しないようで xargs kill はエラーになった。心配なら再起動なり、X から一旦ログインすれば多分大丈夫。

最後の nohup nemo … は RabbitVCS のログがコンソールに流れないようになるようだ。普通に Nemo を起動しても動作に支障はなさそう。

使ってみて思い出したが、そもそも TortoiseSVN の動きが感覚的に好きでないのと、右クリックするとップアップメニューに RabbitVCS の項目が追加されるのだが、かなりの頻度でメニューの改変がされないことがありストレスを感じた。この現象は、PCのスペック不足が原因かもしれないので問題ない人は気にしなくていい。

で結局削除して kdesvn をインストールすることにした。こっちは、SrouceTree のような感じといったらわかりやすいだろうか。

参考

[rabbitvcs/clients/nemo/README at master · rabbitvcs/rabbitvcs](https://github.com/rabbitvcs/rabbitvcs/blob/master/clients/nemo/README)

Linux Mint で btrfs にスワップファイルを作成する

Android エミュレータを起動するとメモリフルで張り付いてしまった。確認してみるとスワップファイルがなかったので再作成した話。

わかってしまえば簡単なのですが、btrfs の場合 ext4 の操作に追加して行うことがあるらしい。

この例では、/ が btrfs パーティションです。最初の 3 行を予め実行しておかないと swapon で
”swapon: swapfile2: swapon failed: Invalid argument” とエラーが出てスワップ領域が有効になりません。

# truncate -s 0 /swapfile
# chattr +C /swapfile
# btrfs property set /swapfile compression none
# fallocate -l 16G /swapfile
# chomod 600 /swapfile
# mkswap /swapfile
# swapon /swapfile

参考

linux – Swapfile Swapon invalid argument – Unix & Linux Stack Exchange

Btrfs#スワップファイル – ArchWiki

Linux Mint 21にスワップスペースを追加する方法 >> ext4 ならこれだけでいい。

Android ワイアレスデバッグのショートカット設定

また、Android アプリの開発に戻ってきたがあまり好きになれないのは相変わらず。そんなことはさておき、wireless debug の設定画面が深すぎて面倒なのでショートカットを探した話。

しょーもない小ネタではあるのだが、自分用のメモとして残す。

開発者設定の [クイック設定開発者用タイル] で[ワイヤレスでバッグ] をオンにすると、ショートカットが設定できる。端末のメーカーによってはこの設定項目がないらしい。

設定をオンにするとどの画面でも上からスワイプで出てくる画面(これをクイックタイルと呼ぶ?)で切り替えられるようになる。

Synology NAS で Forgejo をちょっと試してみる

自宅 NAS では Gitlab を動かしています。これは機能しているのでいいといえばいいのですが、遅いです。それほど強力でない NAS 上の docker とはいえ起動して使い始めるまで大体 5 分位かかります。

それから、Gitlab のアップデートは異常に早いです。大抵 2、3 週間放置しているとアップデートされ、critical update も非常に頻繁です。そのたびにアップデートするのですが正直面倒なのと失敗するかもしれないというドキドキはあまり嬉しいものではありません。

なのでちょっと違うものを試してみることにしました。Gitea が良さそうと思いましたが、更に Forgejo が fork されているようです。なので Forgejo を動かしてみることにします。

Forgejo は dockerhub でホストされていないようなので、Project を作成します。

Create Project ダイアログは以下のように指定します。

Project name: 適当な名前
Path: NAS 上の composer.yaml を置く場所
Source: docker-compose.yaml の作成を選択する
テキストボックスに docker-compose.yaml の内容を指定して [Next] へ進む
Web ポータルの設定はせずに先に進む。

version: '3'

networks:
  forgejo:
    external: false

services:
  server:
    image: codeberg.org/forgejo/forgejo:7
    container_name: forgejo
    environment:
      - USER_UID=1000
      - USER_GID=1000
    restart: always
    networks:
      - forgejo
    volumes:
      - /volume1/docker/forgejo:/data
      #- /etc/timezone:/etc/timezone:ro
      #- /etc/localtime:/etc/localtime:ro
    ports:
      - '8002:3000'
      - '8003:22'

ポート番号は適当に変えています。適宜、状況に合わせて調整してください。

リバースプロキシ設定で https://forgejo.xxx.myds.me を 8002 に流して https の設定は終わり。

ssh 接続は確認していないので後ほど追記することになると思う。

参考

Installation with Docker | Forgejo – Beyond coding. We forge.

synology の Docker で code-server を動かす

タブレットをせっかく買ったので個人開発をタブレットでもできるようにということでいろいろと調べてみる。

coder っていう Web IDE が見つかり、それはどうやら VS Code を動かしているものっぽい。その coder が出している code-server ってので Docker で動くらしいのでやってみた。

自分の環境では、Docker と Synology NAS のリバースプロキシを使っているが Docker だけの場合は R.P. 部分は読み飛ばせばいい。リバースプロキシで SSL しているので Docker 側では何もしていない。

Docker 設定

Docker の設定は、 Synology NAS にならう感じで書いてくので、いい感じで読み替えて Dockerfile なりを書いてほしい。

イメージ

codercom/code-server を duckerhub からダウンロードする

ポート

ここでは、ホスト側 9001 をコンテナ側 8080 になるように expose した。

ボリューム

ここは、dockerhub の説明とは少し変えた。というのもボリュームリンクなしで起動してみると /home/config の中には、.config や、 .local の他にもいろいろ保存されそうなだったのでざっくりと指定した。まとめると

ホスト側はあくまで自分の環境でのことなので、好きな場所に変えてください。

ホスト側コンテナ側
/volume1/docker/code-server/config/home/coder
/volume1/docker/code-server/project/home/project

ここまでで、 9001 ポートに http アクセスすれば使えるようになっている。

ログインパスワード

コンテナははじめからパスワード認証が有効になっている。そのパスワードは初期値が config.yaml に生成されるようだ。

場所は上の例で行くと、/volume1/docker/code-server/config/.config/code-server/config.yaml です。

リバースプロキシ設定

ここも、synology NAS のでやる場合の内容です。

外側 443ポートに対しネームベースで振り分けるように設定します。また、code-server は WebSocket を使っているらしいので ws が通るように設定します。

xxx.myds.me は、NAS に付属の DDNS ホスト名です。そのサブドメインを作成し、コンテナに流す設定をします。

コンテナは、先程 9001 を開けたので 9001 に流します。

[ソース側]

Protocol: HTTPS
Hostname: code.xxx.myds.me
Port: 443

[転送先 (destination) ]

Protocol: HTTP
Hostname: localhost
Port: 9001

[カスタムヘッダー]

WebSocket を通すためにカスタムヘッダーを設定します。 Synology のリバースプロキシの実態は nginx なのでその設定と言ったらわかりやすいでしょうか?

Custom Header タブに行き Create > Websocket を選択すうと結果こうなります

Upgrade => $http_upgrade
Connection => $connection_upgrade

ここまで終えると、

https:/code.xxx.myds.me/ でアクセルすると、VS-Code っぽい画面がでてきます。初回はパスワード認証で先程のパスワードを入力すると認証をパスします。

エディタで開くフォルダは、 ?folder=/home/project とかすればいいようです。場所によってかけなかったり、コンテナ停止で消えたりするので注意が必要ですね。

code-server のコンテナには python とか入っていないので、ちゃんとやりたければ別のコンテナかサーバーに ssh したほうがよさそう。直接コンテナにインスールしても消えてしまう。(コンテナの / をどっかにマウントするのもちょっとなんだし)

参考

codercom/code-server – Docker Image | Docker Hub

Install – code-server Docs

Websoc kets for Synology DSM – Matthias Lohr – Synology のリバースプロキシで websocket を通す方法

ssh ログイン時に “no matching host key type found. Their offer: ssh-rsa” エラー

自宅 PC を Linux にスイッチして2ヶ月位が経ちました。この間小さなハマりは、たくさんありましたがこの話もその 1 つです。

今朝、NEC の ix ルーターに久しぶりにログインしようとするとログインできません。職場から支給されている Windows では問題なくできています。

実際にはこんな感じです。

$ ssh KANRISYA@192.168.1.222
Unable to negotiate with 192.168.1.222 port 22: no matching host key type found. Their offer: ssh-rsa

Qiita によると、ssh のいつかのバージョンから古い鍵タイプはデフォルトでオフされているようです。

解決するには、参考ページのように ~/.ssh/config に追記してやります。コマンドラインのオプションを追加してもいいのですが、ちょっと長いのでつらいです。

  Host ix
       Hostname 192.168.1.222
       HostKeyAlgorithms ssh-rsa

これで無事にアクセスできるようになりました。

参考

ssh 接続で no matching host key type found エラー #SSH – Qiita

Linux Mint に azure-cli をインストールする

例によって新しい仕事に関連して azure-cli をインスールする必要があったのでそのメモ。

Linux Mint の場合 lsb_release コマンドでは Ubuntu コードネームを返さないといいつものトラップがあるのでそれを回避す必要があるよという話。

手順

基本、Microsoft の公式の案内通りですが、Ubuntu のコードネームを手動で設定してやる必要があります。

Linux Mint 21.3 の場合は、 jammy にする必要があります。 このコードネームは /etc/os-release 見るのが一番手っ取り早いと思う。

$ cat  /etc/os-release 
NAME="Linux Mint"
VERSION="21.3 (Virginia)"
ID=linuxmint
ID_LIKE="ubuntu debian"
PRETTY_NAME="Linux Mint 21.3"
VERSION_ID="21.3"
HOME_URL="https://www.linuxmint.com/"
SUPPORT_URL="https://forums.linuxmint.com/"
BUG_REPORT_URL="http://linuxmint-troubleshooting-guide.readthedocs.io/en/latest/"
PRIVACY_POLICY_URL="https://www.linuxmint.com/"
VERSION_CODENAME=virginia
UBUNTU_CODENAME=jammy

こんな感じで jammy とわかる

$ sudo apt update
$ sudo apt-get install apt-transport-https ca-certificates curl gnupg lsb-release
// 上の行はたいてい入っているはずだからいらない

// 鍵をダウンロードする
$ sudo mkdir -p /etc/apt/keyrings
$ curl -sLS https://packages.microsoft.com/keys/microsoft.asc |
  gpg --dearmor | sudo tee /etc/apt/keyrings/microsoft.gpg > /dev/null
sudo chmod go+r /etc/apt/keyrings/microsoft.gpg

//$ AZ_DIST=$(lsb_release -cs) の代わりに直接指定する
$  AZ_DIST=jammy
echo "Types: deb
URIs: https://packages.microsoft.com/repos/azure-cli/
Suites: ${AZ_DIST}
Components: main
Architectures: $(dpkg --print-architecture)
Signed-by: /etc/apt/keyrings/microsoft.gpg" | sudo tee /etc/apt/sources.list.d/azure-cli.sources

$ sudo apt update
$ sudo apt install azure-cli

$ az -v
azure-cli                         2.61.0

core                              2.61.0
telemetry                          1.1.0

Dependencies:
msal                              1.28.0
azure-mgmt-resource               23.1.1

Python location '/opt/az/bin/python3'
Extensions directory '/home/mnishi/.azure/cliextensions'

Python (Linux) 3.11.8 (main, May 16 2024, 03:47:28) [GCC 11.4.0]

Legal docs and information: aka.ms/AzureCliLegal


Your CLI is up-to-date.

こんな感じで実行できる。

こういうケースでアップストリームのコードネームを取れるコマンドってきっとありそうだが、知らないので少し気にして調べてみよう。

今度のプロジェクトでは C#が必要らしいが、Linux の Mono でいいんか?んー、よくわかりません。

参考

Azure CLI を Linux にインストールする | Microsoft Learn