このサイトは、只今WEB業界で活躍中のデザイナー、プログラマーの方々の情報を集めたweb統合情報サイトです。

Archives Details

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その7 macOS ネイティブメニュー・多言語対応・GitHub Actions でのリリース

Rust

2026.10.04

どもです。

R-CHR の解説もいよいよ最終回です。ここまでで CHR の読み書き、UI、描画ツール、PNG インポートと、エディタとしての機能は一通り解説してきました。

最終回は「egui だけでは足りなかった部分をどう埋めたか」という話で、macOS のネイティブメニューバー、日本語 / 英語の切り替え、そして GitHub Actions で Windows / macOS / Linux 向けにビルドして配布する仕組みをまとめた形になります。

作って動くところまでは楽しいのですが、「他の人が普通に使える形で配る」となると途端にやることが増えるんですよね。。

連載の目次

  1. 概要とアーキテクチャ
  2. NES の CHR フォーマット(2BPP)と iNES ヘッダ、パレット
  3. egui で組む 3 パネル UI とバンクビュー
  4. ドットエディタの基礎とアンドゥ設計
  5. 図形ツール(Bresenham・楕円・塗りつぶし・スタンプ)
  6. PNG インポートと 3 つのマッピング戦略
  7. macOS ネイティブメニュー・多言語対応・GitHub Actions でのリリース(この記事)

macOS ではネイティブメニューバーを使いたい

egui にはメニューバーのウィジェット(egui::menu::bar)があって、Windows や Linux ではそれで十分です。ただ macOS の場合、メニューはウィンドウの中ではなく画面上部のメニューバーにあるのが当たり前でして、ウィンドウ内に egui のメニューが出ていると、なんというか「Mac のアプリではない感」がすごい。

あと、Cmd+S や Cmd+Z のようなショートカットも、macOS ではメニュー項目に紐づいたアクセラレータとして扱われるのが自然です。

ということで、macOS だけ muda クレートでネイティブ NSMenu を組み、それ以外の OS は egui のメニューバー、と切り替えています。

// ── メニューバー (macOS はネイティブメニューを使うため非表示)
#[cfg(not(target_os = "macos"))]
self.show_menu_bar(ctx);

native_menu.rs は先頭に #![cfg(target_os = "macos")] を付けてファイルごと macOS 専用にし、Cargo.toml でも依存を [target.'cfg(target_os = "macos")'.dependencies] に入れているので、他の OS では muda や objc2 がそもそもビルドされません。

初期化タイミングの制約

muda で NSMenu を組んでアプリに登録するには menu.init_for_nsapp() を呼ぶのですが、これは NSApplication が生成された後でないと動きません。そして NSApplication を作っているのは eframe の run_native() の中です。

なので、初期化は run_native() に渡すアプリ生成クロージャの中で行います。第 3 回の main.rs にあったこれですね。

eframe::run_native(
    "R-CHR",
    options,
    Box::new(|cc| {
        // macOS: NSApp が初期化された後(ここ)でネイティブメニューを構築し外観を設定する
        #[cfg(target_os = "macos")]
        {
            native_menu::init();
            native_menu::set_app_appearance(true); // デフォルトはダークモード
        }
        // ...
    }),
)

run_native() の前で呼ぶと NSApp が無くて落ちる、というのを最初にやらかしました。

MenuItem は Send ではない

もうひとつの罠が、muda の MenuItem が内部で Rc を使っていて Send ではないことです。メニュー項目のハンドル(あとで有効 / 無効を切り替えたりテキストを変えたりするために必要)をグローバルに持ちたいのですが、static や OnceLock には Send + Sync が要求されるので入れられません。

そこで thread_local! + RefCell です。

struct MenuHandles {
    about:            MenuItem,
    lang_english:     CheckMenuItem,
    file_new:         MenuItem,
    file_open:        MenuItem,
    // ...
    view_dark_mode:   CheckMenuItem,
    // サブメニュー(言語切替時に set_text するため保持)
    sub_file:         Submenu,
    sub_edit:         Submenu,
    sub_view:         Submenu,
    sub_palette:      Submenu,
}

thread_local! {
    static HANDLES: RefCell<<option>> = const { RefCell::new(None) };
}

メニューはメインスレッドからしか触らないので、スレッドローカルで持つのは理にかなっています。

先頭はアプリ名メニュー

macOS のメニューバーは、最初のサブメニューが必ずアプリ名のメニュー(About や Quit が入るやつ)として扱われるというルールがあります。これを知らずにファイルメニューを先頭に置くと、「ファイル」の中身がアプリ名メニューに吸い込まれます。

// ── macOS: 先頭はアプリ名メニュー(省略するとファイルメニューがそこに入る)
let app_menu = Submenu::new("R-CHR", true);
app_menu.append(&h.about).unwrap();
app_menu.append(&PredefinedMenuItem::separator()).unwrap();
app_menu.append(&h.lang_english).unwrap();
app_menu.append(&PredefinedMenuItem::separator()).unwrap();
app_menu.append(&PredefinedMenuItem::quit(None)).unwrap();

// ── ルートメニューに追加してアプリのメニューバーへ
let menu = Menu::new();
menu.append(&app_menu).unwrap();
menu.append(&h.sub_file).unwrap();
menu.append(&h.sub_edit).unwrap();
menu.append(&h.sub_view).unwrap();
menu.append(&h.sub_palette).unwrap();
menu.init_for_nsapp();

ショートカットは Accelerator で項目に付けます。

file_save: MenuItem::new(s.file_save, false, Some(Accelerator::new(Some(cmd), Code::KeyS))),
イベントの受け取り

muda のメニューはクリックされると MenuEvent::receiver() のチャネルにイベントを流します。R-CHR ではこれを毎フレーム try_recv() でポーリングして、項目 ID を照合して自前の MenuAction enum に変換しています。

pub fn try_recv_action() -> Option {
    let event = MenuEvent::receiver().try_recv().ok()?;
    HANDLES.with(|slot| {
        let borrow = slot.borrow();
        let h = borrow.as_ref()?;
        let id = &event.id;
        if      id == h.about.id()            { Some(MenuAction::About) }
        else if id == h.file_new.id()         { Some(MenuAction::FileNew) }
        else if id == h.file_open.id()        { Some(MenuAction::FileOpen) }
        // ...
        else if id == h.view_dark_mode.id()   { Some(MenuAction::ViewDarkMode(h.view_dark_mode.is_checked())) }
        else { None }
    })
}

受け取り側は update() の先頭で、キューが空になるまで回してディスパッチします。

pub(in crate::editor) fn handle_native_menu(&mut self, ctx: &egui::Context) {
    while let Some(action) = native_menu::try_recv_action() {
        match action {
            MenuAction::About           => self.show_about = true,
            MenuAction::FileNew         => self.new_file(),
            MenuAction::FileOpen        => self.open_file(),
            MenuAction::FileSave        => { if let Err(e) = self.save_file() { self.error_msg = Some(e); } }
            MenuAction::EditUndo        => self.do_undo(),
            MenuAction::ViewDarkMode(v) => {
                self.dark_mode = v;
                native_menu::set_app_appearance(v);
            }
            MenuAction::LangEnglish(en) => {
                self.lang = if en { Lang::En } else { Lang::Ja };
                native_menu::set_menu_lang(self.lang);
            }
            // ...
        }
    }

    // enabled / checked 状態を毎フレーム同期
    let has_tile = self.selected_tile.is_some()
        && self.rom.as_ref().map_or(false, |r| !r.chr_data().is_empty());
    native_menu::sync_state(
        self.file_path.is_some() && self.is_modified,
        !self.undo_stack.is_empty(),
        has_tile,
        has_tile && self.tile_clipboard.is_some(),
        self.dark_mode,
        self.lang,
    );
}

native_menu が返すのはあくまで「何が押されたか」だけで、アプリの状態は一切知りません。アプリ側のディスパッチは editor/mac/menu_events.rs に置いてあります。

enabled / checked の同期

「未保存の変更が無いときは保存をグレーアウト」「アンドゥスタックが空なら元に戻すをグレーアウト」といった状態は、毎フレーム sync_state() で egui 側の状態からメニューへ書き込む形にしています。

pub fn sync_state(can_save: bool, can_undo: bool, can_copy: bool, can_paste: bool, dark_mode: bool, lang: Lang) {
    HANDLES.with(|slot| {
        let borrow = slot.borrow();
        let Some(h) = borrow.as_ref() else { return };
        h.file_save.set_enabled(can_save);
        h.edit_undo.set_enabled(can_undo);
        h.edit_copy.set_enabled(can_copy);
        h.edit_paste.set_enabled(can_paste);
        if h.view_dark_mode.is_checked() != dark_mode {
            h.view_dark_mode.set_checked(dark_mode);
        }
        // ...
    });
}

即時モードの発想をネイティブメニューにも持ち込んだ形ですね。「状態が変わったらメニューを更新する」のではなく「毎フレーム状態をメニューに流し込む」。イベント駆動で更新漏れを気にするより、こっちのほうが圧倒的に楽でした。

キーボードショートカットの二重定義

ここでひとつ注意点。macOS で NSMenu にアクセラレータを付けると、Cmd+Z や Cmd+S は NSMenu が先に横取りして、egui には届きません。なので第 3 回で見た keyboard.rs の Cmd+Z 処理は、macOS ではメニュー経由で動いています。

keyboard.rs にも同じ処理を残しているのは、NSMenu が機能しない状況へのフォールバックです。二重に実行される心配は無いので(届くのはどちらか一方だけ)、そのままにしてあります。

ダークモードと NSAlert

タイトルバーの色まで含めてダークモードにするには、egui の Visuals だけでは足りず、NSApplication の外観を設定する必要があります。objc2-app-kit で NSAppearance を切り替えています。

pub fn set_app_appearance(dark: bool) {
    let Some(mtm) = MainThreadMarker::new() else { return };
    let app = NSApplication::sharedApplication(mtm);
    let name = if dark {
        unsafe { NSAppearanceNameDarkAqua }
    } else {
        unsafe { NSAppearanceNameAqua }
    };
    let appearance = NSAppearance::appearanceNamed(name);
    unsafe { app.setAppearance(appearance.as_deref()); }
}

未保存の変更があるときの確認ダイアログも、macOS では NSAlert を直接呼んでいます。

pub fn unsaved_changes_dialog(file_name: &str, lang: Lang) -> UnsavedChoice {
    let Some(mtm) = MainThreadMarker::new() else {
        return UnsavedChoice::Cancel;
    };
    let s = crate::editor::i18n::t(lang);
    let alert = unsafe { NSAlert::new(mtm) };
    unsafe {
        alert.setMessageText(&NSString::from_str(s.unsaved_title));
        alert.setInformativeText(&NSString::from_str(&lang.fmt_unsaved_body(file_name)));
        alert.setAlertStyle(NSAlertStyle::Warning);
        alert.addButtonWithTitle(&NSString::from_str(s.save_and_close));
        alert.addButtonWithTitle(&NSString::from_str(s.discard_btn));
        alert.addButtonWithTitle(&NSString::from_str(s.cancel_btn2));
        let response = alert.runModal();
        // NSAlertFirstButtonReturn=1000, Second=1001, Third=1002
        match response {
            1000 => UnsavedChoice::Save,
            1001 => UnsavedChoice::Discard,
            _    => UnsavedChoice::Cancel,
        }
    }
}

objc2 経由だと Objective-C の API がほぼそのまま Rust から呼べます。unsafe だらけにはなりますが、MainThreadMarker で「メインスレッドからしか呼べない」ことが型で保証されるのは、さすが Rust だなぁと。

macOS 以外では、同じ確認を rfd::MessageDialog の Yes / No / Cancel で出しています。

日本語 / 英語の切り替え

UI の文言は日本語と英語を切り替えられます。仕組みはとても素朴で、全ての静的文字列を 1 つの構造体にまとめて、言語ごとに static インスタンスを持つだけです。

#[derive(Clone, Copy, PartialEq, Eq, Default)]
pub enum Lang { #[default] Ja, En }

/// UI で使う全静的文字列のセット
pub struct Strings {
    pub about:              &'static str,
    pub menu_file:          &'static str,
    pub file_new:           &'static str,
    pub file_open:          &'static str,
    // ...(60 個ほど)
}

static JA: Strings = Strings {
    about:              "R-CHR について…",
    menu_file:          "ファイル",
    file_new:           "新規作成",
    file_open:          "開く…",
    // ...
};

static EN: Strings = Strings { /* ... */ };

pub fn t(lang: Lang) -> &'static Strings {
    match lang { Lang::Ja => &JA, Lang::En => &EN }
}

使う側は self.t().file_open のように書きます。全部 &'static str なので実行時のアロケーションはゼロですし、構造体のフィールドなので文言を 1 つ追加したら両言語に追加しないとコンパイルエラーになるのが地味に便利です。翻訳漏れが構造的に起きません。

数値やファイル名を埋め込む文言は、Lang のメソッドとして用意しています。

impl Lang {
    pub fn fmt_png_done(self, tw: usize, th: usize) -> String { /* ... */ }
    pub fn fmt_unsaved_body(self, file_name: &str) -> String { /* ... */ }
    pub fn fmt_approx_colors(self, n: usize) -> String { /* ... */ }
    // ...
}

前回の PngWarning を enum にした話も、この構造に乗せるためでした。

言語を切り替えたときは、egui 側は次のフレームで勝手に新しい文言で描き直されますが、ネイティブメニューは自分で更新する必要があります。それが set_menu_lang() で、保持しているハンドル全部に set_text() を呼んで回ります。サブメニューのハンドルまで MenuHandles に持たせているのはこのためです。

GitHub Actions で 3 OS 向けにビルドしてリリース

最後に配布の話です。main に push すると、Windows / macOS / Linux の 3 つでビルドして、GitHub Releases に成果物を並べるところまで自動化しています。

name: Build & Release

on:
  push:
    branches:
      - main

jobs:
  build:
    name: Build (${{ matrix.os }})
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            artifact_name: R-CHR
            asset_name: R-CHR-linux-x86_64
            asset_path: R-CHR-linux-x86_64
          - os: windows-latest
            artifact_name: R-CHR.exe
            asset_name: R-CHR-windows-x86_64.exe
            asset_path: R-CHR-windows-x86_64.exe
          - os: macos-latest
            artifact_name: R-CHR
            asset_name: R-CHR-macos-aarch64
            asset_path: R-CHR-macos-aarch64.zip

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable

      - name: Cache cargo
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-

      - name: Install Linux system dependencies
        if: matrix.os == 'ubuntu-latest'
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libxcb-render0-dev \
            libxcb-shape0-dev \
            libxcb-xfixes0-dev \
            libxkbcommon-dev \
            libssl-dev \
            libgtk-3-dev

      - name: Build
        run: cargo build --release

fail-fast: false にしているのは、1 つの OS でコケても他の OS のビルドは最後まで走らせたいからです。Linux だけ apt-get で X11 / GTK 系のヘッダを入れているのは、eframe(winit)と rfd(GTK のファイルダイアログ)のビルドに必要なためですね。ここ、ローカルの Mac では一切気にしなくて良いので、CI で初めて気づくやつです。

macos-latest は現在 Apple Silicon のランナーなので、成果物は aarch64 になります。

macOS は .app バンドルにする

Windows と Linux は cargo build で出てきたバイナリをそのまま置けば良いのですが、macOS はそうもいかず、.app バンドルの形にしないとダブルクリックで起動できません(アイコンも付きません)。

      - name: Package (macOS .app bundle)
        if: matrix.os == 'macos-latest'
        run: |
          APP="R-CHR.app"
          VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')

          # ディレクトリ構造
          mkdir -p "$APP/Contents/MacOS"
          mkdir -p "$APP/Contents/Resources"

          # バイナリ
          cp target/release/R-CHR "$APP/Contents/MacOS/R-CHR"

          # Info.plist(バージョンを埋め込む)
          sed "s/{{VERSION}}/$VERSION/g" macos/Info.plist > "$APP/Contents/Info.plist"

          # icon.png → AppIcon.icns
          ICONSET="AppIcon.iconset"
          mkdir -p "$ICONSET"
          for size in 16 32 128 256 512; do
            sips -z $size $size assets/icon.png --out "$ICONSET/icon_${size}x${size}.png"
            sips -z $((size * 2)) $((size * 2)) assets/icon.png --out "$ICONSET/icon_${size}x${size}@2x.png"
          done
          iconutil -c icns "$ICONSET" -o "$APP/Contents/Resources/AppIcon.icns"
          rm -rf "$ICONSET"

          # ad-hoc 署名(Gatekeeper の「壊れている」エラーを回避)
          codesign --force --deep --sign - "$APP"

          # ditto で zip(パーミッション・拡張属性を保持)
          ditto -c -k --keepParent "$APP" ${{ matrix.asset_path }}

やっていることを順に。

  1. ディレクトリ構造:.app は実体としてはただのディレクトリで、Contents/MacOS/ にバイナリ、Contents/Resources/ にアイコン、Contents/Info.plist にメタ情報、という決まった構造です
  2. Info.plist:リポジトリに {{VERSION}} というプレースホルダ入りのテンプレートを置いておき、Cargo.toml から取ったバージョンを sed で埋めます。バージョンの管理場所を Cargo.toml 一箇所にするためです
  3. アイコン:assets/icon.png から sips で各サイズの PNG を作って .iconset ディレクトリに並べ、iconutil で .icns に固めます。macOS 標準ツールだけで完結します
  4. ad-hoc 署名:ここが重要でして、署名なしの .app を zip で配ると、受け取った側で「壊れているため開けません」と言われます。Apple Developer の証明書は無くても、codesign --sign - の ad-hoc 署名をしておくだけでこのエラーは回避できます(「開発元を確認できません」の警告は出るので、右クリック → 開く、は必要です)
  5. ditto で zip:普通の zip コマンドだと実行権限や拡張属性が落ちてしまうことがあるので、macOS の ditto -c -k --keepParent で固めます

Info.plist はこんなテンプレートです。





    CFBundleName
    R-CHR
    CFBundleDisplayName
    R-CHR
    CFBundleIdentifier
    com.rchr.app
    CFBundleVersion
    {{VERSION}}
    CFBundleShortVersionString
    {{VERSION}}
    CFBundleExecutable
    R-CHR
    CFBundlePackageType
    APPL
    CFBundleIconFile
    AppIcon
    NSHighResolutionCapable
    
    LSMinimumSystemVersion
    11.0

NSHighResolutionCapable を入れておかないと Retina でボケボケになるので、これは必須です。

Release ジョブ

3 OS のビルドが終わったら、別ジョブで成果物を集めて Release を作ります。

  release:
    name: Create Release
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Get version from Cargo.toml
        id: version
        run: |
          VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
          echo "tag=v${VERSION}" >> $GITHUB_OUTPUT
          echo "name=v${VERSION}" >> $GITHUB_OUTPUT

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts
          merge-multiple: true

      - name: Create or update Release
        uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ steps.version.outputs.tag }}
          name: ${{ steps.version.outputs.name }}
          files: artifacts/*
          fail_on_unmatched_files: true
          make_latest: true

タグ名も Cargo.toml のバージョンから作っています。v0.1.0 のようなタグの Release が無ければ作り、あれば成果物を更新する、という動きです。なのでリリースしたいときは Cargo.toml の version を上げて push するだけ。タグを手で打つ必要はありません。

permissions: contents: write を忘れると Release の作成で権限エラーになるので、ここも CI で初めて気づくやつですね。

連載のまとめ

7 回にわたって R-CHR の中身を解説してきました。振り返ると、設計上の判断で効いたのはこの辺りかなと思います。

  • 層を分ける:model → io → editor の一方通行にして、CHR の変換や PNG のマッピングを egui から切り離した。おかげで純粋なデータ処理として書けて、あとから読み返しても分かりやすい
  • 書き込みの出口を 1 つにする:全ツールの書き込みが encode_dot()、アンドゥの入口が push_undo_batch()。ツールを足すのが「ドット座標のリストを作る関数を書く」だけになった
  • UI とデータ更新を分ける:EditorAction で「何をするか」だけを返して、描画後に適用する。即時モード + 借用チェッカーと仲良くやる定石
  • 即時モードの発想を外にも広げる:ネイティブメニューの enabled / checked も「毎フレーム状態を流し込む」にしたら、更新漏れを考えなくて良くなった
  • 配布まで含めて自動化する:.app バンドルと ad-hoc 署名まで CI に入れておいたので、Cargo.toml のバージョンを上げるだけでリリースできる

Rust + egui でデスクトップアプリを配布用としてきちんと作るのは初めてだったのですが、思っていたより素直に組めた、というのが正直な感想です。特にドットエディタのような「状態から絵を描き直す」タイプのアプリと即時モードの相性は本当に良いので、同じようなツールを作ろうとしている方には結構おすすめできるのかと。

当初は「YY-CHR の代わりに Mac で使えるものがあれば」くらいの気持ちで始めたのですが、気がついたら描画ツールが 10 個になり、PNG インポートに 3 つも戦略が付き、macOS の NSAlert まで呼んでいました。まぁ、こういうものですよねw

リポジトリはこちらです。バグ報告や要望などあれば Issue にいただけると嬉しいです。

ではではぁ。

またまたぁ。

Comment

Related Article

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その7 macOS ネイティブメニュー・多言語対応・GitHub Actions でのリリース

2026.10.04

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その6 PNG インポートと 3 つのマッピング戦略

2026.10.02

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その5 図形ツール(Bresenham・楕円・塗りつぶし・スタンプ)

2026.09.30

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その4 ドットエディタの基礎とアンドゥ設計

2026.09.28

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その3 egui で組む 3 パネル UI とバンクビュー

2026.09.26

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その2 CHR の 2BPP フォーマットと iNES ヘッダ、パレット

2026.09.24

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その1 概要とアーキテクチャ

2026.09.22

ゲーム開発に必要な基本数学入門

2025.08.15

Godot 4 & Rust 始め方

2025.06.19

Rustでつくる ふっかつのじゅもん

2025.04.29

CATEGORY LIST

LATEST NEWS

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その7 macOS ネイティブメニュー・多言語対応・GitHub Actions でのリリース

Rust

2026.10.04

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その6 PNG インポートと 3 つのマッピング戦略

Rust

2026.10.02

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その5 図形ツール(Bresenham・楕円・塗りつぶし・スタンプ)

Rust

2026.09.30

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その4 ドットエディタの基礎とアンドゥ設計

Rust

2026.09.28

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その3 egui で組む 3 パネル UI とバンクビュー

Rust

2026.09.26

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その2 CHR の 2BPP フォーマットと iNES ヘッダ、パレット

Rust

2026.09.24

【R-CHR】Rust + egui でファミコン用スプライトエディタを作った話 その1 概要とアーキテクチャ

Rust

2026.09.22

M5Stack Stopwatchで、オリジナル LT(Lightning Talks)タイマーを作成した。

電子工作

2026.09.21

【Railway】GitHub Actions で定期的にサーバーをリスタートさせる

運用

2026.08.30

ラズパイが高騰しているのでストレス解消のためAliExpressで購入した電子パーツ達

電子工作

2026.08.02

【2026】【VMware Fusion】MacでWindowsを無料で扱う。

windows

2026.07.19

Raspberry Pi 5 でマインクラフトサーバーを立てる(Java版 × 統合版クロスプレイ対応)

RaspberryPi

2026.06.24

RANKING

Follow

SPONSOR

現在、掲載募集中です。



Links

About Us

WEBデザイナーの、WEBデザイナーによる、WEBデザイナーの為のサイト。「みんなで書こう!」と仲間を募ってみたが、結局書くのは自分だけとなってしまいました。日々のメモを綴っていきます。

Entry Profile

Graphical FrontEnd Engineer
- Daisuke Takayama

MAD CITY 北九州市で生まれ育つ。20代はバンド活動に明け暮れ、ふと「webデザイナーになりたい。」と思い、デジタルハリウッド福岡校入学。卒業後、数々の賞を受賞、web業界をざわつかせる。
現在、主に、ゲーム制作中心に港区六本木界隈で活動中。

FOLLOW US