ltk/event_loop/
text_scale.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

//! Follow the desktop's accessibility text scale.
//!
//! Reads `org.gnome.desktop.interface text-scaling-factor` once at
//! startup and then follows external changes through `gsettings
//! monitor`, sending each value to the run loop, which applies it via
//! [`crate::set_text_scale`] and repaints. GTK apps track this key on
//! their own; this module gives every ltk app the same behaviour with
//! no app-side wiring. Degrades silently to a fixed 1.0 when
//! `gsettings` is not installed.

use std::io::{ BufRead, BufReader };
use std::os::unix::process::CommandExt;
use std::process::{ Command, Stdio };

const SCHEMA: &str = "org.gnome.desktop.interface";
const KEY:    &str = "text-scaling-factor";

/// A blocked signal mask survives exec, so clear it: a child that
/// inherits SIGTERM blocked outlives the session's shutdown.
fn unblock_signals( cmd: &mut Command ) -> &mut Command
{
	unsafe
	{
		cmd.pre_exec( ||
		{
			let mut set: libc::sigset_t = std::mem::zeroed();
			libc::sigemptyset( &mut set );
			libc::sigprocmask( libc::SIG_SETMASK, &set, std::ptr::null_mut() );
			Ok( () )
		} )
	}
}

/// Spawn the watcher thread. `tx` delivers each observed factor to the
/// run loop; the initial `gsettings get` value is sent first.
pub( super ) fn spawn_watcher( tx: calloop::channel::Sender<f32> )
{
	let _ = std::thread::Builder::new()
		.name( "ltk-text-scale".into() )
		.spawn( move ||
		{
			if let Some( v ) = read_current()
			{
				let _ = tx.send( v );
			}

			let Ok( mut child ) = unblock_signals(
					Command::new( "gsettings" )
						.args( [ "monitor", SCHEMA, KEY ] )
						.stdout( Stdio::piped() )
						.stderr( Stdio::null() ) )
				.spawn()
			else { return };
			let Some( stdout ) = child.stdout.take() else { return };

			// Each line has the shape `text-scaling-factor: 1.25`.
			for line in BufReader::new( stdout ).lines().map_while( Result::ok )
			{
				if let Some( v ) = parse_factor( &line )
				{
					if tx.send( v ).is_err() { break; }
				}
			}
			let _ = child.kill();
		} );
}

fn read_current() -> Option<f32>
{
	let out = unblock_signals(
			Command::new( "gsettings" )
				.args( [ "get", SCHEMA, KEY ] )
				.stderr( Stdio::null() ) )
		.output()
		.ok()?;
	parse_factor( std::str::from_utf8( &out.stdout ).ok()? )
}

fn parse_factor( s: &str ) -> Option<f32>
{
	s.rsplit( [ ' ', ':' ] )
		.next()?
		.trim()
		.parse::<f32>()
		.ok()
		.filter( |v| *v > 0.0 )
}

#[ cfg( test ) ]
mod tests
{
	use super::{ parse_factor, unblock_signals };

	#[ test ]
	fn parses_monitor_line_and_bare_value()
	{
		assert_eq!( parse_factor( "text-scaling-factor: 1.25" ), Some( 1.25 ) );
		assert_eq!( parse_factor( "1.0\n" ), Some( 1.0 ) );
		assert_eq!( parse_factor( "" ), None );
		assert_eq!( parse_factor( "text-scaling-factor: nope" ), None );
	}

	fn spawn_and_read_sigblk( clear: bool ) -> u64
	{
		let mut cmd = std::process::Command::new( "sleep" );
		cmd.arg( "30" );
		if clear
		{
			unblock_signals( &mut cmd );
		}
		let mut child = cmd.spawn().unwrap();
		let status = std::fs::read_to_string( format!( "/proc/{}/status", child.id() ) ).unwrap();
		let _ = child.kill();
		let _ = child.wait();
		status.lines()
			.find_map( |l| l.strip_prefix( "SigBlk:" ) )
			.and_then( |v| u64::from_str_radix( v.trim(), 16 ).ok() )
			.unwrap()
	}

	#[ test ]
	fn child_starts_with_clear_signal_mask()
	{
		unsafe
		{
			let mut set: libc::sigset_t = std::mem::zeroed();
			libc::sigemptyset( &mut set );
			libc::sigaddset( &mut set, libc::SIGTERM );
			libc::pthread_sigmask( libc::SIG_BLOCK, &set, std::ptr::null_mut() );
		}
		let sigterm_bit = 1u64 << ( libc::SIGTERM - 1 );

		// std deliberately lets the child inherit the mask — the very
		// reason unblock_signals exists.
		assert_ne!( spawn_and_read_sigblk( false ) & sigterm_bit, 0 );
		assert_eq!( spawn_and_read_sigblk( true ) & sigterm_bit, 0 );
	}
}