「QR コードを表示している間だけ画面を明るくする」「バッテリー駆動になったら暗くする」のように、アプリから画面の明るさを変えたい場面があります。Tauri 自体に輝度の API は無いので、Rust の brightness クレートを使います。対応するのは Windows と Linux で、ノート PC の内蔵ディスプレイと、DDC/CI(モニターを PC から操作する仕組み)に対応した外部モニターが対象です。変わるのは画面全体の明るさなので、アプリの表示だけを暗くしたいなら CSS の filter: brightness(0.7) で足ります。
前提条件
brightness 0.8 は macOS では「unsupported platform」というエラーでビルドが止まります。macOS にも配るアプリでは、src-tauri/Cargo.toml の対象 OS を限定した欄に入れます。非同期の一覧を扱うため futures も使います。
[target.'cfg(any(windows, target_os = "linux"))'.dependencies]
brightness = "0.8"
futures = "0.3"
自作コマンドの invoke だけなので、capability の追加は不要です。画面の種類と OS による条件は次のとおりです。
| 画面 | Windows | Linux |
|---|---|---|
| ノート PC の内蔵 | そのまま使える。決まった段階に丸められる | /sys/class/backlight に出ていれば使える |
| 外部モニター | モニターの DDC/CI が有効なら使える | ddcci-backlight ドライバーが必要 |
1. バックエンドから実装する (Rust)
押さえる点は 3 つです。
- 一覧に出ても変えられるとは限らない: Windows では DDC/CI に対応していないモニターも一覧に出て、
get()の段階で失敗します。読めた画面だけを操作対象にします。 - 1 台の失敗で打ち切らない: 一覧は
Resultの列で届きます。最初のエラーで止めず、全部集めてから振り分けます。 - 原因まで出す: エラーの
to_string()は「どの画面で失敗したか」だけなので、source()をたどって原因をつなげます。
macOS 用には同じ名前の関数を持つ空のモジュールを用意し、コマンドの登録はどの OS でも同じにします。
use serde::Serialize;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct ScreenBrightness {
id: String, // 変更するときに渡す識別名
name: String, // 表示用(Windows はモニターの製品名)
percent: Option<u32>, // 読めなかった画面は None
error: Option<String>,
}
#[cfg(any(windows, target_os = "linux"))]
mod screen {
use super::ScreenBrightness;
use brightness::{Brightness, BrightnessDevice};
use futures::StreamExt;
use std::error::Error as _;
fn describe(e: &brightness::Error) -> String {
let mut msg = e.to_string();
let mut cause = e.source();
while let Some(c) = cause {
msg = format!("{msg}: {c}");
cause = c.source();
}
msg
}
/// 1 台の失敗で打ち切らないよう、Result のまま全部集める
async fn devices() -> Vec<Result<BrightnessDevice, brightness::Error>> {
brightness::brightness_devices().collect().await
}
pub async fn list() -> Vec<ScreenBrightness> {
let mut out = Vec::new();
for dev in devices().await.into_iter().flatten() {
let Ok(id) = dev.device_name().await else { continue };
let name = dev.friendly_device_name().await.unwrap_or_else(|_| id.clone());
let (percent, error) = match dev.get().await {
Ok(p) => (Some(p), None),
Err(e) => (None, Some(describe(&e))),
};
out.push(ScreenBrightness { id, name, percent, error });
}
out
}
pub async fn set(id: &str, percent: u32) -> Result<(), String> {
for mut dev in devices().await.into_iter().flatten() {
if dev.device_name().await.ok().as_deref() == Some(id) {
return dev.set(percent).await.map_err(|e| describe(&e));
}
}
Err(format!("画面が見つかりません: {id}"))
}
}
/// brightness が対応していない OS(macOS など)では「変えられる画面なし」として扱う
#[cfg(not(any(windows, target_os = "linux")))]
mod screen {
use super::ScreenBrightness;
pub async fn list() -> Vec<ScreenBrightness> {
Vec::new()
}
pub async fn set(_id: &str, _percent: u32) -> Result<(), String> {
Err("この OS では明るさを変更できません".into())
}
}
#[tauri::command]
async fn list_screen_brightness() -> Vec<ScreenBrightness> {
screen::list().await
}
#[tauri::command]
async fn set_screen_brightness(id: String, percent: u32) -> Result<(), String> {
// 機種によっては 0% でバックライトが消えて何も見えなくなるので、下限を設ける
screen::set(&id, percent.clamp(5, 100)).await
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.invoke_handler(tauri::generate_handler![list_screen_brightness, set_screen_brightness])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
どちらも async コマンドなので、外部モニターとの通信を待つ間も画面は固まりません(重い処理を別スレッド・非同期で実行する)。
2. フロントエンドから呼び出す (TypeScript)
画面ごとにスライダーを並べます。外部モニターとの通信は、読み取りだけでも 1 回に数十ミリ秒かかります。スライダーの input のたびに送ると処理がたまるので、動きが止まってから送ります。
import { invoke } from '@tauri-apps/api/core';
type ScreenBrightness = { id: string; name: string; percent: number | null; error: string | null };
export async function renderBrightness(root: HTMLElement) {
const screens = await invoke<ScreenBrightness[]>('list_screen_brightness');
root.replaceChildren();
if (screens.length === 0) {
root.textContent = '明るさを変更できる画面がありません';
return;
}
for (const s of screens) {
const row = document.createElement('label');
row.append(`${s.name} `);
if (s.percent === null) {
// DDC/CI に対応していないモニターなど。操作させず、理由はツールチップに出す
row.append('(変更できません)');
row.title = s.error ?? '';
root.append(row);
continue;
}
const slider = document.createElement('input');
slider.type = 'range';
slider.min = '5';
slider.max = '100';
slider.value = String(s.percent);
let timer: number | undefined;
slider.addEventListener('input', () => {
window.clearTimeout(timer);
timer = window.setTimeout(() => {
invoke('set_screen_brightness', { id: s.id, percent: Number(slider.value) })
.catch((e) => console.error('明るさを変えられませんでした:', e));
}, 200);
});
row.append(slider);
root.append(row);
}
}
await renderBrightness(document.querySelector<HTMLElement>('#brightness') ?? document.body);
一時的に明るくする用途では、変える前の percent を控えておき、終わったら戻します。Windows の id は \\.\DISPLAY1\Monitor0 の形で、モニター情報 の currentMonitor() が返す name(\\.\DISPLAY1)で始まるので、ウィンドウが表示されている画面を選べます。
動作確認
npm run tauri dev で起動し、一覧を 1 行ずつ出します。
import { invoke } from '@tauri-apps/api/core';
type ScreenBrightness = { id: string; name: string; percent: number | null; error: string | null };
for (const s of await invoke<ScreenBrightness[]>('list_screen_brightness')) {
console.log(`${s.id} | ${s.name} | ${s.percent ?? '-'}% | ${s.error ?? ''}`);
}
DDC/CI に対応した外部モニター 1 台の Windows では次のように出ます。DDC/CI で変わるのはモニター本体の設定なので、スライダーを動かすと、モニターのメニューにある明るさの値も同じように変わります。
\\.\DISPLAY1\Monitor0 | Generic Monitor | 50% |
よくあるエラーと対処法
- macOS で「unsupported platform」: brightness を通常の
[dependencies]に入れています。前提条件のように対象 OS を限定します。 - 外部モニターの
percentが null: エラーは「Failed to get brightness device\\.\DISPLAY2\Monitor0information: Failed to get monitor brightness (DDCCI)」で始まります。モニターのメニューで DDC/CI を有効にし、ドッキングステーションや変換アダプター、KVM スイッチを通さずに PC へ直接つないで試します。テレビや一部のモニターは DDC/CI に対応していないことがあります。 - Linux で一覧が空:
/sys/class/backlightに画面がありません。デスクトップ PC の外部モニターは、ddcci-backlight ドライバーを入れると現れます。 - Linux で「Setting brightness failed for device intel_backlight」: 設定はログイン中のセッションを通して行われ、使えないときは
/sys/class/backlight/<名前>/brightnessへの書き込みに切り替わります。SSH 越しなどセッションの外から動かしていないか確かめ、必要なら udev ルールで書き込み権限を与えます。 - リモートデスクトップ接続中に一覧が空: Windows では、リモート接続の仮想の画面は対象になりません。
OS ごとの違いと注意点
- Windows: 内蔵ディスプレイは決まった段階の中から近い値に丸められます。明るさの自動調整が有効だと、設定した値が OS に上書きされることがあります。
- Linux: 値は整数の百分率に切り捨てて返ります。ノート PC で
acpi_video0とintel_backlightのように同じ画面が 2 つ出て、効くのが片方だけのことがあります。 - macOS: brightness は対応していません。例では一覧が空になるので、OS の種類を取得 して明るさの項目ごと隠すと親切です。
- 共通: 変更は画面全体に効き、アプリを終了しても戻りません。バッテリー駆動への切り替えで明るさを変えるなら バッテリー状態と電源接続を取得・監視する と組み合わせます。
