Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ HUD を実機で見るなら `./scripts/local_check.sh`(オプションは `do
```bash
./scripts/visual_regression.sh --update
```
- VRT が撮るのは HUD の contentView と設定ウィンドウの各ペイン(`run_settings_case`。タブバーは contentView の外なので写らない)。メニューバーと About パネルは対象外(システム所有の表面で view を PNG 化できない。About は渡す中身を UT で固定している)
- VRT が撮るのは HUD の contentView、設定ウィンドウの各ペイン、設定タブの行スタック全体(`run_settings_case`。ペインはタブバーが contentView の外なので写らず、行スタック全体は `NSScrollView` の documentView を可視域でクリップせず撮る)。メニューバーと About パネルは対象外(システム所有の表面で view を PNG 化できない。About は渡す中身を UT で固定している)
- VRT のケースは `CLIIP_SHOW_*` 環境変数で設定を上書きして作る(`scripts/visual_regression.sh` の `run_case`)。設定ウィンドウのケースは `CLIIP_SHOW_LANGUAGE` を必ず明示する(`auto` は実行マシンの言語に依存し、ローカルと CI でベースラインが食い違う)
- `Cargo.toml` の `edition` を変えたら `.claude/settings.json` の rustfmt フック(`--edition` を直書き)も同期する。ズレるとフックの整形結果を CI の `cargo fmt --check` が拒否する

Expand Down
2 changes: 2 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ Apple の Developer ID で署名していないため、cask のインストー

起動中にもう一度起動すると、2 つ目は立ち上がらず設定ウィンドウが開きます。

設定ウィンドウの「メニューバーにアイコンを表示」でメニューバーのアイコンを非表示にできます。非表示にするとメニュー(設定・一時停止・終了)にも触れなくなりますが、アプリをもう一度起動すると設定ウィンドウが前面に出るので、そこから再び表示できます。

### formula から乗り換える

formula は cask に置き換わったので、先にアンインストールします。
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Launching it shows "Cliip Show started" in the HUD. It stays quiet when launched

Launching it again while it is running opens the settings window instead of starting a second copy.

You can hide the menu bar icon from "Show icon in menu bar" in the settings window. If you do, that also cuts off the menu (settings, pause, quit); launch the app again to bring the settings window to the front and show the icon again.

### Coming from the formula

The formula is replaced by the cask, so uninstall it first:
Expand Down
3 changes: 3 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ cliip-show --config-show
- `hud_emoji`(既定値: `📋`、任意の絵文字。空でアイコンなし)
- `hud_image_max_height`(既定値: `160`、`40` - `240`)画像サムネイルの高さ上限(px)。実際の上限は `hud_scale` 倍され、元画像より大きくは表示しません
- `language`(既定値: `auto`、`auto` / `ja` / `en`)メニューバーと設定ウィンドウの表示言語。`auto` は macOS の優先言語に従います
- `show_menu_bar_icon`(既定値: `true`)メニューバーアイコンの表示。`false` にすると、メニューから開く設定・一時停止・終了に触れなくなります。アプリをもう一度起動すると設定ウィンドウが前面に出るので、そこから表示に戻せます

設定キー(`[startup]` 節):
- `start_at_login`(既定値: `false`)設定ウィンドウの「ログイン時に自動起動」と同じ値です。LaunchAgent の plist はこの値から書き出すため、plist を手で消してもこの値が残っていれば次の起動で書き直します
Expand All @@ -72,6 +73,7 @@ CLIIP_SHOW_HUD_BACKGROUND_OPACITY=0.6 \
CLIIP_SHOW_HUD_EMOJI=🍣 \
CLIIP_SHOW_HUD_IMAGE_MAX_HEIGHT=120 \
CLIIP_SHOW_LANGUAGE=en \
CLIIP_SHOW_SHOW_MENU_BAR_ICON=true \
cargo run
```

Expand Down Expand Up @@ -137,6 +139,7 @@ HUDと設定ウィンドウの各ペインの描画結果をPNGで比較しま
- デフォルト設定での表示
- 設定プロファイルごとの表示(例: `max_lines=2`, `max_chars_per_line=24`)
- 設定ウィンドウのペイン(設定・寄付 × 日本語・英語。タブバーはウィンドウのツールバー領域にあるため写りません)
- 設定タブの行スタック全体(`NSScrollView` の documentView。可視域でクリップされず全行が1枚に収まります。フッターのボタン列は documentView の外にあるため写りません)

### 生成物

Expand Down
18 changes: 16 additions & 2 deletions scripts/visual_regression.sh
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ render_and_compare() {
-u CLIIP_SHOW_HUD_IMAGE_MAX_HEIGHT
-u CLIIP_SHOW_HUD_EMOJI
-u CLIIP_SHOW_LANGUAGE
-u CLIIP_SHOW_SHOW_MENU_BAR_ICON
"CLIIP_SHOW_CONFIG_PATH=$VRT_CONFIG_PATH"
)
if [[ $# -gt 0 ]]; then
Expand Down Expand Up @@ -143,8 +144,9 @@ run_image_case() {
render_and_compare "$id" --render-hud-png --image-fixture "$size" "$@"
}

# 設定ウィンドウのペイン(settings | support)。タブバーは contentView の外なので写らない。
# 言語は必ず CLIIP_SHOW_LANGUAGE で固定して呼ぶこと(auto は実行マシンの言語に依存する)
# 設定ウィンドウのペイン(settings | settings-rows | support)。タブバーは contentView の外
# なので写らない。言語は必ず CLIIP_SHOW_LANGUAGE で固定して呼ぶこと(auto は実行マシンの
# 言語に依存する)
run_settings_case() {
local id="$1"
local pane="$2"
Expand Down Expand Up @@ -365,6 +367,18 @@ run_settings_case \
"settings" \
"CLIIP_SHOW_LANGUAGE=en"

# Settings tab document view: full row stack (NSScrollView documentView), unclipped by the
# visible scroll area, so rows below the fold are captured too.
run_settings_case \
"settings_rows_ja" \
"settings-rows" \
"CLIIP_SHOW_LANGUAGE=ja"

run_settings_case \
"settings_rows_en" \
"settings-rows" \
"CLIIP_SHOW_LANGUAGE=en"

run_settings_case \
"support_pane_ja" \
"support" \
Expand Down
17 changes: 17 additions & 0 deletions src/app/config_reload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,9 +140,26 @@ pub(crate) unsafe fn apply_settings_now(state: &mut AppState, new_settings: Disp
super::schedule_poll_timer(state.delegate, new_settings.poll_interval_secs);
}

let menu_bar_icon_changed =
new_settings.show_menu_bar_icon != state.settings.show_menu_bar_icon;
let language_changed = new_settings.language != state.settings.language;
state.settings = new_settings;

// show_menu_bar_icon が変わったら status item の表示と設定ウィンドウのトグルを即時更新。
// 手編集・環境変数・GUI のトグル操作のいずれもここを通るため、更新箇所は一箇所で済む。
if menu_bar_icon_changed {
crate::menu::apply_menu_bar_icon_visibility(
state.menu_handles.status.status_item,
state.settings.show_menu_bar_icon,
);
crate::settings_window::sync_menu_bar_icon_toggle(
&state.settings_controls,
state.settings.show_menu_bar_icon,
);
// draft は「保存」で丸ごと書き戻されるため、古い値で上書きしないようここでも揃える。
state.settings_controls.draft.show_menu_bar_icon = state.settings.show_menu_bar_icon;
}

// language が変わったら設定ウィンドウとメニューの文言を即時更新。
// ファイル監視の再読み込みと設定ウィンドウの言語ポップアップのどちらの経路もここを
// 通るため、更新箇所は一箇所で済む。
Expand Down
71 changes: 69 additions & 2 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ pub fn get_delegate_class() -> &'static AnyClass {
sel!(toggleLoginItem:),
toggle_login_item as extern "C" fn(_, _, _),
);
builder.add_method(
sel!(toggleMenuBarIcon:),
toggle_menu_bar_icon as extern "C" fn(_, _, _),
);
builder.add_method(
sel!(showEmojiHelp:),
show_emoji_help as extern "C" fn(_, _, _),
Expand Down Expand Up @@ -155,6 +159,12 @@ extern "C" fn application_did_finish_launching(this: &AnyObject, _: Sel, _: *mut
}

let status_handles = crate::menu::create_status_item(this, lang);
// NSStatusItem.visible は AppKit 側が永続化するため、差分判定なしで毎回設定を当てる
// (前回の残骸で消えたままになるのを防ぐ)。
crate::menu::apply_menu_bar_icon_visibility(
status_handles.status_item,
settings.show_menu_bar_icon,
);
let menu_handles = MenuHandles {
status: status_handles,
edit: edit_handles,
Expand Down Expand Up @@ -302,8 +312,14 @@ extern "C" fn open_settings(this: &AnyObject, _: Sel, _: *mut AnyObject) {

if state.settings_controls.window.is_null() {
let lang = i18n::resolve(state.settings.language);
state.settings_controls =
crate::settings_window::build_settings_window(this, lang, state.start_at_login);
state.settings_controls = crate::settings_window::build_settings_window(
this,
lang,
crate::settings_window::InitialToggles {
start_at_login: state.start_at_login,
show_menu_bar_icon: state.settings.show_menu_bar_icon,
},
);
}

// 既に開いているウィンドウは前面に出すだけにする。他のインスタンスの起動でも
Expand Down Expand Up @@ -413,6 +429,56 @@ fn persist_start_at_login(state: &mut AppState, value: bool) -> bool {
true
}

/// メニューバーアイコン表示トグルの `toggleMenuBarIcon:`。ログイン項目トグルと同じく下書きを
/// 経由せず、チェックした瞬間に設定ファイルへ保存し `apply_settings_now` で status item・
/// 設定ウィンドウへ即時反映する。非表示にした場合だけ、復帰方法(アプリを再起動すると
/// 設定ウィンドウが開く)を知らせるダイアログを出す。
extern "C" fn toggle_menu_bar_icon(_: &AnyObject, _: Sel, sender: *mut AnyObject) {
unsafe {
let hidden_dialog_lang = {
// AppKit メインスレッドからのみ呼ばれるため、Mutex が poison されるケースは実質発生しない
let mut guard = APP_STATE.lock().expect("APP_STATE lock poisoned");
let Some(state) = guard.as_mut() else {
return;
};

let checked: isize = msg_send![sender, state];
let desired = checked != 0;
let was_visible = state.settings.show_menu_bar_icon;

match crate::settings_window::save_show_menu_bar_icon(state, desired) {
Ok(new_settings) => {
apply_settings_now(state, new_settings);
// 環境変数が show_menu_bar_icon を固定していると、設定ファイルへ保存しても
// 実効値は動かない。トグルの表示を実効値へ揃え、ダイアログも実際に
// アイコンが消えたときだけ出す。
let visible = state.settings.show_menu_bar_icon;
crate::settings_window::sync_menu_bar_icon_toggle(
&state.settings_controls,
visible,
);
(was_visible && !visible).then(|| i18n::resolve(state.settings.language))
}
Err(error) => {
eprintln!("warning: {error}");
// 失敗したのにチェックだけ付いた状態を避け、実際の状態に表示を戻す。
crate::settings_window::sync_menu_bar_icon_toggle(
&state.settings_controls,
state.settings.show_menu_bar_icon,
);
None
}
}
};

// runModal は実行ループを止めるため、APP_STATE のロックを手放した後に呼ぶ
// (prompt_login_item_if_needed と同じ理由)。
if let Some(lang) = hidden_dialog_lang {
panels::prompt_menu_bar_icon_hidden(lang);
}
}
}

// NSRectEdge: maxX。ボタン/フィールドの右側に popover を出す(`show_emoji_help_popover`・
// `show_range_hint_popover` で共用)。
const NS_RECT_EDGE_MAX_X: usize = 2;
Expand Down Expand Up @@ -831,6 +897,7 @@ mod tests {
sel!(previewSettings:),
sel!(saveSettings:),
sel!(toggleLoginItem:),
sel!(toggleMenuBarIcon:),
sel!(showEmojiHelp:),
sel!(closeRangeHintPopover:),
sel!(windowWillClose:),
Expand Down
27 changes: 27 additions & 0 deletions src/app/panels.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,33 @@ pub(super) unsafe fn prompt_login_item_if_needed(lang: Lang, start_at_login: boo
}
}

/// メニューバーのアイコンを非表示にした直後に出す、復帰方法の案内ダイアログ。
/// ボタンは追加しない(NSAlert の既定である「OK」1つで足りる)。抑止チェックボックスも
/// 持たない。この経路は「今しがた非表示にした」操作の直後にしか出ないため、
/// `prompt_login_item_if_needed` のように繰り返し表示を避ける仕組みは不要。
///
/// # Safety
/// - `APP_STATE` のロックを保持したまま呼ばないこと。`runModal` が実行ループを止めるため、
/// 他の経路がロック待ちで固まる。
/// - AppKit のメインスレッドから呼ぶこと。
pub(super) unsafe fn prompt_menu_bar_icon_hidden(lang: Lang) {
// accessory アプリ(setActivationPolicy: 1)はこれを呼ばないとダイアログが前面に来ない
let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication];
let () = msg_send![app, activateIgnoringOtherApps: true];

let alert: *mut AnyObject = msg_send![class!(NSAlert), alloc];
let alert: *mut AnyObject = msg_send![alert, init];

let message = NSString::from_str(i18n::text(lang, Msg::MenuBarIconHiddenMessage));
let () = msg_send![alert, setMessageText: &*message];

let informative = NSString::from_str(i18n::text(lang, Msg::MenuBarIconHiddenDetail));
let () = msg_send![alert, setInformativeText: &*informative];

let _: isize = msg_send![alert, runModal];
let () = msg_send![alert, release];
}

/// macOS 標準の About パネルを出す。バンドルを持たないので、表示内容は options で渡す。
pub(super) extern "C" fn show_about_panel(_: &AnyObject, _: Sel, _: *mut AnyObject) {
unsafe {
Expand Down
20 changes: 17 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,10 +106,12 @@ fn parse_settings_png_args(args: impl Iterator<Item = String>) -> Result<Setting
};
pane = Some(match value.as_str() {
"settings" => SettingsPane::Settings,
"settings-rows" => SettingsPane::SettingsRows,
"support" => SettingsPane::Support,
unknown => {
return Err(format!(
"Invalid value for --pane: {unknown} (expected settings or support)"
"Invalid value for --pane: {unknown} \
(expected settings, settings-rows, or support)"
));
}
});
Expand Down Expand Up @@ -225,7 +227,7 @@ pub fn handle_cli_flags() -> bool {
);
let _ = writeln!(
help,
" --render-settings-png --pane <settings|support> --output <PATH> Render settings window pane PNG and exit"
" --render-settings-png --pane <settings|settings-rows|support> --output <PATH> Render settings window pane PNG and exit"
);
let _ = writeln!(
help,
Expand Down Expand Up @@ -305,6 +307,10 @@ pub fn handle_cli_flags() -> bool {
help,
" CLIIP_SHOW_LANGUAGE UI language (auto|ja|en)"
);
let _ = writeln!(
help,
" CLIIP_SHOW_SHOW_MENU_BAR_ICON Show the menu bar icon (true|false)"
);
print!("{help}");
true
}
Expand Down Expand Up @@ -605,13 +611,21 @@ mod tests {
.map(|parsed| parsed.pane),
Ok(SettingsPane::Settings)
);
assert_eq!(
parse_settings_png_args(args(&["--pane", "settings-rows", "--output", "/tmp/s.png"]))
.map(|parsed| parsed.pane),
Ok(SettingsPane::SettingsRows)
);
}

#[test]
fn settings_args_reject_invalid_or_missing_options() {
assert_eq!(
parse_settings_png_args(args(&["--pane", "about", "--output", "/tmp/s.png"])),
Err("Invalid value for --pane: about (expected settings or support)".to_string())
Err(
"Invalid value for --pane: about (expected settings, settings-rows, or support)"
.to_string()
)
);
assert_eq!(
parse_settings_png_args(args(&["--output", "/tmp/s.png"])),
Expand Down
Loading
Loading