記事一覧

技術メモを残していきます

svg-line: EmacsのステータスバーをSVGで統一する試み

元記事 svg-line: Better Status Bars for Emacs Emacsのmode-line、header-line、tab-bar、tab-lineはそれぞれネイティブAPIレベルで挙動が違い、多段表示・右寄せ・アイコン・マウスイベントの扱いがバラバラという問題がある。svg-lineはこれらをSVG画像として描画することで挙動を統一するパッケージ。 SVGにすることで座標ベースのマウスイベント検出ができるのも地味に大きい。ネイティブAPIだとクリックやホバーが*-lineごとに対応がバラバラだったのが一気に解決される。 この記事に触発されて私も少し触ってみようと思った。 あわよくばこのまま置き換えてもいいかとも思う。 やったこと 既存パッケージの整理 svg-lineを試すために一旦以下を削除・調整した。 nyan-mode → 削除 minions → 削除 spacious-padding → :mode-line-widthの行だけ削除(他の余白設定は残す) svg-lineの導入 (use-package svg-line :straight (:host github :repo "chiply/svg-line")) mode-lineを書いてみた 最小構成から始めて、バッファ名・git branch・メジャーモード・マイナーモード・行列数の2行構成を目指した。 (svg-line-define 'my-mode-line :target 'mode-line :active #'mode-line-window-selected-p :background (lambda () (face-background 'mode-line nil t)) :foreground (lambda () (face-foreground 'mode-line nil t)) :content (lambda () (list (list :left (list (if (buffer-modified-p) "● " " ") (buffer-name)) :right (list (or (and (fboundp 'vc-git--symbolic-ref) (buffer-file-name) (vc-git--symbolic-ref (buffer-file-name))) ""))) (cons (list (symbol-name major-mode)) (list (format-mode-line "%l:%c")))))) (svg-line-activate 'my-mode-line) シンプルにはなったが、やはりnyan-modeがないとさみしい。 ...

June 9, 2026 · 1 min

.zshrcのチューニング: 203msから79msへ

きっかけ Life is too short for a slow terminal を読んだ。 とりあえず「自分のzshも計測してみるか」となった。 流石にTerminalとかGPUパワーでFPS改善するとかフレームワーク使うのやめるだとか、ガッツリオリジナルコード書きまくるほどではないにしても明確にコレは駄目だというものがあれば改善したい。 先程の記事の筆者はoh-my-zshもpreztoも使わない主義で30msを達成しているが、私はp10kのUIを捨てるコストは払いたくなかったので、フレームワーク(zinit + p10k)は維持したまま改善できる部分だけ潰す方針にした。 まず計測 time zsh -i -c exit zsh -i -c exit 0.09s user 0.07s system 75% cpu 0.203 total 203ms。遅くはないが伸びしろがある気がする。 zprof で犯人を特定する .zshrc の先頭に: zmodload zsh/zprof 末尾に: zprof を追加して新しいシェルを開くと、関数ごとの所要時間テーブルが出る。上位30件だけ見れば十分。 zprof | head -n 30 num calls time self name ----------------------------------------------------------------------------------- 1) 1518 209.95 0.14 15.98% 153.70 0.10 11.70% :zinit-tmp-subst-zle 2) 60 107.81 1.80 8.20% 87.84 1.46 6.68% _zsh_autosuggest_async_request 3) 4 146.30 36.57 11.13% 67.91 16.98 5.17% _zsh_autosuggest_bind_widgets 4) 796 78.39 0.10 5.96% 67.84 0.09 5.16% _zsh_autosuggest_bind_widget 5) 34 98.12 2.89 7.47% 63.19 1.86 4.81% -fast-highlight-process 6) 2407 59.89 0.02 4.56% 59.89 0.02 4.56% .zinit-add-report ... zinit-tmp-subst-zle が1518回呼ばれていて1位。zinit がZLEウィジェットを差し替えるオーバーヘッドで、これはフレームワーク起因なので直接は触れない。 ...

June 7, 2026 · 2 min

WireGuard 経由で UNEXT が見れない問題を解決した話

環境 ラズパイ(Linux)がルーター兼 WireGuard クライアント 配下のデバイス(タブレット等)は eth0 経由でラズパイを通してインターネットへ ラズパイは wlan0 で ISP ルーター(192.168.x.1)に接続 全トラフィックを WireGuard(wg0)経由で VPS に流す構成 VPS は国内 VPS サービス タブレット(192.168.x.x) └─ eth0 ─ ラズパイ(ルーター) ├─ wlan0 ─ ISPルーター ─ インターネット └─ wg0 ─ VPS(国内) ─ インターネット 症状 WireGuard 経由の WiFi で UNEXT が一切見れない YouTube・Amazon Prime Video は問題なし UNEXT の生配信は見れる、VOD だけ駄目 調査 tcpdump で通信を確認 タブレットが UNEXT に接続しようとしたタイミングで tcpdump を仕掛けた。 sudo tcpdump -i eth0 -n 'src <タブレットIP> or dst <タブレットIP>' 2>/dev/null UNEXT のサーバーへの SYN が2回送られているが SYN-ACK が返ってこないことを確認。接続確立できていない。 ...

June 6, 2026 · 2 min

WireGuard 経由で UNEXT が見れない問題を調査した話【調査編】

解決編はこちら → WireGuard 経由で UNEXT が見れない問題を解決した話 | 怠惰技術ブログ 概要 WireGuard 経由の WiFi で UNEXT の VOD だけ見れないという問題を調査した。症状・仮説・コマンド・結果の思考トレースを残しておく。 最初の症状整理 UNEXT の VOD が見れない(くるくるのままタイムアウト) 生配信は見れる YouTube・Prime Video は問題なし この時点での仮説: MTU の問題(大きいパケットが通らない) QUIC(UDP)の問題 DRM 認証の問題 VPS 側のブロック Step 1: MTU を疑う WireGuard はオーバーヘッドがあるので MTU が小さくなる。大きいパケットが詰まってないか確認。 ping -M do -s 1400 8.8.8.8 結果: From 192.168.x.254 icmp_seq=1 Frag needed and DF set (mtu = 1420) ping: sendmsg: Message too long 1400 バイトは通らない。 1300 バイトは通った。MTU の壁が確認できた。 TCPMSS clamping を確認 sudo iptables -t mangle -L FORWARD -n -v TCPMSS tcp -- * * 0.0.0.0/0 0.0.0.0/0 tcp flags:0x06/0x02 TCPMSS clamp to PMTU すでに設定済みだった。TCP はケアされている。 ...

June 6, 2026 · 2 min

EDINET API v2 で有価証券報告書を自動取得する(Node.js / TypeScript)

EDINET は金融庁が運営する電子開示システムで、上場企業が提出した有価証券報告書・四半期報告書などを無償で取得できる API を提供している。 個人開発の財務分析ツールを作るにあたって、この API を Node.js / TypeScript で叩いた際のポイントをまとめる。 エンドポイント概要 ベース URL:https://api.edinet-fsa.go.jp/api/v2 用途 エンドポイント 書類一覧 GET /documents.json?date=YYYY-MM-DD&type=2 PDF取得 GET /documents/{docID}?type=2 XBRL取得 GET /documents/{docID}?type=1(ZIP) API キーはクエリパラメータ Subscription-Key で渡す。EDINET のサイト からアカウント登録すると発行される。ハードコードせず process.env.EDINET_API_KEY から読むのはもはや最低限のマナーといえる。 レスポンスの型定義 まず API レスポンスに型をつける。docID(大文字)が EDINET 公式の表記: interface EdinetDocumentResponse { docID: string // EDINET が振る書類ID docTypeCode: string | null secCode: string | null // 証券コード。上場企業以外は null edinetCode: string filerName: string docDescription: string | null submitDateTime: string } クライアントクラスで型を明示しておくと、後続のフィルタリングや保存ロジックで補完が効いて安全になる。 書類一覧の取得とフィルタリング /documents.json は指定日に提出されたすべての書類を返す。有価証券報告書だけを絞り込むには docTypeCode を見る: 120:有価証券報告書 130:訂正有価証券報告書 140:四半期報告書 また secCode が null の書類は上場企業以外なのでスキップする。 const filteredResults = results.filter((r: EdinetDocumentResponse) => { const typeCode = (r.docTypeCode ?? '').replace(/['"]/g, '') return ( r.secCode != null && (typeCode === '120' || typeCode === '130' || typeCode === '140') ) }) docTypeCode に余分なクォートが混入することがある("120" のように入ってくる)ので replace で除去している。実際にハマった。 ...

May 24, 2026 · 2 min

Gemini API で財務書類を「怪しさ判定」する:スコア付き出力の設計

個人開発の EDINET 分析ツールでは、取得した有価証券報告書の PDF を Gemini に渡して「怪しさ判定」をさせている。 単なる要約ではなく、3段階のスコア(normal / caution / danger) を返させる設計にしたので、その仕組みをまとめる。 なぜスコアが必要か 毎日数十〜数百件の書類が提出される。全部読むのは無理なので、AI に「これは要注意」かどうかを仕分けさせたい。 スコアが danger の書類だけ Discord 通知を飛ばす、といった使い方ができる。 プロンプト設計 プロンプトの末尾に必ずスコアを出力させるよう指示する: 分析の最後に必ず以下の形式でスコアを出力してください: SCORE:normal # 特に問題なし SCORE:caution # 気になる点あり・要確認 SCORE:danger # 重大なリスクの可能性 Gemini はマークダウン形式で分析テキストを返した後、最終行に SCORE:danger のような文字列を出力する。 PDF を渡す方法 @google/generative-ai SDK では PDF を base64 で渡せる: const model = genAI.getGenerativeModel({ model: 'gemini-2.5-flash' }) const result = await model.generateContent([ { inlineData: { mimeType: 'application/pdf', data: pdfBuffer.toString('base64'), }, }, { text: prompt }, ]) 最大 50MB まで渡せるが、大きすぎるとトークン消費が跳ね上がるので注意。 スコアのパース 正規表現で SCORE: 以降を抽出: ...

May 24, 2026 · 1 min

Hono + TypeScript でクリーンアーキテクチャもどきを個人開発に持ち込む

個人開発に「クリーンアーキテクチャ」は過剰では?という気持ちはある。 ただ実際にやってみたら、テストが書きやすい・外部APIの差し替えが楽という恩恵がちゃんとあった。 Hono + TypeScript (ESM) でどう組んだかをメモしておく。 ディレクトリ構成 backend/src/ ├── domain/ # エンティティ・リポジトリ Interface │ ├── entity/ │ └── repository/ ├── usecase/ # ビジネスロジック ├── infrastructure/ # DB・外部API の実装 │ ├── postgres/ │ ├── edinet/ │ └── gemini/ ├── api/ # Hono ルーター └── job/ # JobRunner 依存の方向 api / job ↓ usecase ← domain (Interface) ↓ infrastructure → domain (Interface を実装) usecase は domain の Interface にしか依存しない。 infrastructure が Interface を実装する。これだけ守れば十分。 ...

May 24, 2026 · 2 min

Node.js でバックグラウンドジョブを自前実装する:PostgreSQL でジョブ管理

BullMQ や外部キューサービスを使わずに、PostgreSQL + while(true) ループでバックグラウンドジョブを管理する仕組みを作った。 「外部依存を増やしたくない」「DB を見るだけでジョブの状態がわかるようにしたい」という理由から。 ⚠️ この実装の前提条件(割り切りポイント) この仕組みは 「予算を抑えたい個人開発」かつ「アプリ単一インスタンス(1プロセス)」 での運用を前提に、あえてシンプルに作っています。 以下のトレードオフを理解した上で使ってください。 1. 厳密な重複排除はしていない(レースコンディション) アプリ側で isAnyRunning をチェック → ジョブ作成という 2 ステップになっているため、ミリ秒単位で同時リクエストが来た場合はすり抜ける可能性があります。 厳密に防ぐなら、後述する DB 側の Partial Unique Index が必要です。 2. マルチインスタンス非対応 起動時にジョブを一括リセットしているため、コンテナを複数台並列で動かす(水平拡張する)場合は、他インスタンスで実行中のジョブを巻き添えにします。 複数台にするなら worker_id カラムを導入するか、一括リセットをやめてください。 「バグ」ではなく「この規模だからこその意図的な割り切り」です。BullMQ を検討する規模になったら移行サインと考えています。 jobs テーブルの設計 CREATE TABLE jobs ( id SERIAL PRIMARY KEY, type VARCHAR(50) NOT NULL, status VARCHAR(20) NOT NULL DEFAULT 'pending', log TEXT, started_at TIMESTAMPTZ, finished_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); status は pending → running → done / error と遷移する。 log カラムにジョブの実行ログを蓄積するので、Web UI から確認できる。 ...

May 24, 2026 · 2 min

ReactのuseStateでDate.now()を使うとlintエラーになる話

何が起きたか useState の初期値で Date.now() を直接呼んでいたら、こんなエラーが出た。 error Error: Cannot call impure function during render `Date.now` is an impure function. コード的にはこういうやつ。 const [stockPriceFrom, setStockPriceFrom] = useState<string>( new Date(Date.now() - 90 * 86400000).toISOString().split('T')[0] ) なぜエラーになるか React のルールとして、レンダー中はコンポーネントが pure でなければならない。 Date.now() や Math.random() はレンダーのたびに異なる値を返す「impure な関数」なので、直接渡すと React(特に Strict Mode)に怒られる。 Strict Mode ではレンダーを意図的に 2 回実行するため、こういった副作用が顕在化しやすい。 解決策:lazy initialization useState に関数を渡すと、初回マウント時に一度だけ実行される。これが lazy initialization パターン。 const [stockPriceFrom, setStockPriceFrom] = useState<string>( () => new Date(Date.now() - 90 * 86400000).toISOString().split('T')[0] ) const [stockPriceTo, setStockPriceTo] = useState<string>( () => new Date(Date.now() - 84 * 86400000).toISOString().split('T')[0] ) () => で包むだけ。それだけ。 まとめ パターン 評価タイミング useState(Date.now()) レンダーごとに評価される useState(() => Date.now()) 初回マウント時のみ new Date() も同様なので、日付系の初期値を useState に渡すときはアロー関数で包む癖をつけておくと良い。 ...

May 23, 2026 · 1 min

モダンオペレーティングシステム 第3章中盤メモ

モダンオペレーティングシステム 第5版 上 第3章中盤(3.3後半〜3.5)。ページテーブルの実装とTLB、ページ置き換えアルゴリズム。 3.3後半 ページテーブルの実装 多段ページテーブル 仮想アドレス空間が64bitの場合、単純なページテーブルは現実的じゃない。 理論上の最大:2^64 = 18,446,744,073,709,551,616 バイト 1ページ = 4KB = 4096バイト ページ数 = 2^64 / 2^12 = 2^52 個 1エントリ = 8バイトとして ページテーブルのサイズ = 2^52 × 8 = 32ペタバイト プロセス1個のページテーブルだけで32PB。話にならない。 実際のx86_64は64bitフルを使わず48bitに妥協している(一部の最新CPUは57bit)。64bitフルを使うと6〜7段の階層が必要になりメモリアクセスのオーバーヘッドが耐えられなくなるから。「理論上の最大は64bitだけど、現実のCPUは48bitに妥協している」という設計判断。 Linuxはこれを多段ページテーブルで解決してる。x86_64では4段構成。 仮想アドレス(48bit有効) ┌──────┬──────┬──────┬──────┬────────────┐ │ PGD │ PUD │ PMD │ PTE │ offset │ │ 9bit │ 9bit │ 9bit │ 9bit │ 12bit │ └──────┴──────┴──────┴──────┴────────────┘ 名前は覚えなくていい。住所の階層構造だと思えばいい。 東京都 → PGD(一番大きい区分) 渋谷区 → PUD 代々木1丁目 → PMD 1番地 → PTE 101号室 → offset(ページ内の位置) ポイントは使っていない部分のテーブルを作らないこと。 ...

May 10, 2026 · 4 min