control centre: menus for Wi-Fi, Bluetooth, VPN and Sound in their own window
The tile or block grows into its menu; the panel fades its content and takes the menu's size underneath. Wi-Fi lists networks (join saved/open, disconnect, stop iwd), Bluetooth lists devices, VPN has a switch per openvpn service, Sound has every output with its own slider and a default picker. A second Sound slider for the output named by extra_output in shell.conf (NR515). Screen and keyboard light are written to sysfs directly: logind refuses a shell without a seat.
This commit is contained in:
@@ -20,13 +20,14 @@ The pill shows Wi-Fi signal (the hotspot sign while it's on), the VPN lock (acce
|
||||
|
||||
The panel has:
|
||||
|
||||
- **Four tiles:** Wi-Fi, Bluetooth, VPN, Hotspot. Accent = on, grey = the service behind it isn't running. A grey tile is clickable when it can start that service.
|
||||
- *Wi-Fi* (iwd): iwd stopped → grey, a click runs `rc-service iwd start`. iwd running → a click turns the device (station) on or off. The shell never wakes iwd by asking it anything: it checks `NameHasOwner` first, because any D-Bus call to `net.connman.iwd` starts it.
|
||||
- *Bluetooth* (BlueZ, through Amane's `Bluetooth`): on/off. With bluez installed but stopped, a click runs `rc-service bluetooth start`; without bluez the tile says so.
|
||||
- *VPN*: the openrc services `/etc/init.d/openvpn.*` (here `openvpn.swiss`). Connecting while openrc has it *inactive*, connected once its `up.sh` marks it *started*. A click runs `rc-service openvpn.<name> start|stop`.
|
||||
- **Four tiles:** Wi-Fi, Bluetooth, VPN, Hotspot. Accent = on, grey = the service behind it isn't running. Wi-Fi, Bluetooth and VPN open their menu when clicked anywhere; Hotspot switches at once.
|
||||
- *Wi-Fi* (iwd): iwd stopped → grey; the switch in its menu runs `rc-service iwd start`. iwd running → the switch turns the device (station) on or off. The menu lists the networks in range (connected first, then by signal): click the connected one to disconnect, a saved or open one to join. A new network with a password has to be joined once with `iwctl` (the menu can't ask for passwords yet). "Stop the Wi-Fi service" at the bottom runs `rc-service iwd stop`. The shell never wakes iwd by asking it anything: it checks `NameHasOwner` first, because any D-Bus call to `net.connman.iwd` starts it.
|
||||
- *Bluetooth* (BlueZ, through Amane's `Bluetooth`): the switch powers the adapter; with bluez installed but stopped it runs `rc-service bluetooth start`. The menu scans while it's open and lists the devices: click to connect, disconnect, or pair (pairing works for devices without a PIN).
|
||||
- *VPN*: every openrc service `/etc/init.d/openvpn.*` (here `openvpn.swiss`), each with a switch. Connecting while openrc has it *inactive*, connected once its `up.sh` marks it *started*. A switch runs `rc-service openvpn.<name> start|stop`.
|
||||
- *Hotspot* (iwd access point): see [Hotspot setup](#hotspot-setup).
|
||||
- **Sound:** the default output's volume (drag or scroll the slider); the value next to the title is the mute button.
|
||||
- **Display:** the laptop screen's brightness (through logind, like Amane's `Brightness`) and the keyboard light (`tpacpi::kbd_backlight`: Off, Low, High). For external monitors see [External monitor brightness](#external-monitor-brightness).
|
||||
- **Menus:** each opens in a window of its own over the panel and grows out of its tile; the panel takes the menu's size underneath, so the two lie on top of each other. The back button returns to the panel; moving the pointer off both closes everything.
|
||||
- **Sound:** the default output's volume (drag or scroll the slider); the value next to the title is the mute button. With `extra_output` in `shell.conf` a second slider follows for the output whose name contains it (here `NR515`: the `[TX-NR515]` receiver on HDMI 1). Clicking the block elsewhere opens the Sound menu: every output with its own slider and mute; click a name to make it the default output.
|
||||
- **Display:** the laptop screen's brightness and the keyboard light (`tpacpi::kbd_backlight`: Off, Low, High), written straight to their sysfs files (a udev rule makes them writable by everyone on this laptop), else through logind, which refuses a shell without a seat (one started over ssh). For external monitors see [External monitor brightness](#external-monitor-brightness).
|
||||
- **Performance:** the firmware profiles in `/sys/firmware/acpi/platform_profile_choices` (Low power, Balanced, Performance), and the battery's state and time left. Picking one sets the platform profile and the CPU governor and energy preference with it: Low power = `powersave` + `power`, Balanced = `powersave` + `balance_performance`, Performance = the `performance` governor.
|
||||
- **External GPU:** the GPU the machine didn't boot with (`boot_vga = 0`, here the Radeon RX 6600 in the Thunderbolt enclosure): load, temperature, power draw, video memory, and every program that has it open with its video memory (and its share of the GPU while the driver reports engine time). Found through `/proc/<pid>/fdinfo` (`drm-pdev`), only while the panel is open. Root's processes can't be read and don't show; the shell itself shows up, because Vulkan opens every GPU.
|
||||
|
||||
@@ -34,7 +35,8 @@ Changes that need root (starting services, the performance mode) run as `doas -n
|
||||
|
||||
```sh
|
||||
amane ipc call control toggle # also: open, close
|
||||
amane ipc call control status # expanded=<bool> wifi=<state> vpn=<state> profile=<p> battery=<n%>
|
||||
amane ipc call control menu wifi # also: bluetooth, vpn, sound, close
|
||||
amane ipc call control status # expanded=<bool> menu=<name|none> wifi=<state> vpn=<state> profile=<p> battery=<n%>
|
||||
```
|
||||
|
||||
### Hotspot setup
|
||||
@@ -100,6 +102,7 @@ The Material You scheme type is Skwd-wall's setting (`matugen.schemeType`).
|
||||
cover_colors = false # true: the open media card and equalizer take the song cover's colours as background
|
||||
transition_ms = 400 # how long a colour change cross-fades; 0 switches instantly
|
||||
hotspot = MyHotspot # the iwd access point profile the control centre's hotspot starts
|
||||
extra_output = NR515 # a second Sound slider for the output whose name contains this
|
||||
```
|
||||
|
||||
Keep comments on lines of their own in the real file: a `#` after a value becomes part of it.
|
||||
@@ -119,7 +122,7 @@ src/glyph.rs every drawn icon
|
||||
src/theme.rs the shell's colours: the matugen theme file, shell.conf, the cross-fade
|
||||
src/parse.rs pure text parsing with unit tests, no amane dependency
|
||||
matugen/ the matugen template (amane.colors), rendered by skwd-wall into ./theme
|
||||
shell.conf settings: cover_colors, transition_ms, hotspot
|
||||
shell.conf settings: cover_colors, transition_ms, hotspot, extra_output
|
||||
island-eq saved equalizer curves (a snapshot of ~/.config/amane/island-eq)
|
||||
tools/ remote test driver and fake MPRIS players, see AGENTS.md
|
||||
```
|
||||
|
||||
@@ -11,3 +11,6 @@ transition_ms = 400
|
||||
# the control centre's hotspot: the name of an iwd access point profile, /var/lib/iwd/ap/<name>.ap
|
||||
# (see README "Control centre"); leave it out and the hotspot tile says so
|
||||
# hotspot = MyHotspot
|
||||
|
||||
# a second slider under Sound in the control centre, for the output whose name contains this
|
||||
extra_output = NR515
|
||||
|
||||
+787
-43
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,8 @@ pub enum Glyph {
|
||||
Muted,
|
||||
Bluetooth,
|
||||
Chevron,
|
||||
// pointing left, for a menu's way back
|
||||
Back,
|
||||
Note,
|
||||
|
||||
// wi-fi with 0 to 3 of its arcs lit; the unlit ones are drawn faint
|
||||
@@ -91,6 +93,9 @@ pub fn bold_icon(glyph: Glyph, size: f32, color: Color, weight: f32) -> Canvas {
|
||||
Glyph::Chevron => shapes![
|
||||
Path::new().move_to(0.12 * s, 0.32 * s).line_to(0.5 * s, 0.7 * s).line_to(0.88 * s, 0.32 * s).stroke(1.5, color).cap(Cap::Round)
|
||||
],
|
||||
Glyph::Back => shapes![
|
||||
Path::new().move_to(0.64 * s, 0.14 * s).line_to(0.3 * s, 0.5 * s).line_to(0.64 * s, 0.86 * s).stroke(thin.max(1.6), color).cap(Cap::Round)
|
||||
],
|
||||
// an eighth note: round head, stem, a curved flag
|
||||
Glyph::Note => shapes![
|
||||
Circle::new().center(0.32 * s, 0.74 * s).radius(0.17 * s).fill(color),
|
||||
|
||||
+2
-2
@@ -23,8 +23,8 @@ use crate::theme;
|
||||
* on the settled shape. closing runs the same timeline backwards
|
||||
*/
|
||||
const OPEN_MS: u64 = 560;
|
||||
const PILL_GONE: f32 = 0.18;
|
||||
const SHAPE_DONE: f32 = 0.6;
|
||||
pub const PILL_GONE: f32 = 0.18;
|
||||
pub const SHAPE_DONE: f32 = 0.6;
|
||||
|
||||
// hover highlights ease in and out over this long; the pill widens by PILL_HOVER_GROW
|
||||
pub const HOVER_MS: u64 = 220;
|
||||
|
||||
@@ -24,5 +24,6 @@ fn main() {
|
||||
.window_per_monitor(island::view)
|
||||
.window_per_monitor(workspaces::view)
|
||||
.window_per_monitor(control::view)
|
||||
.window_per_monitor(control::menu_view)
|
||||
.run();
|
||||
}
|
||||
|
||||
@@ -37,6 +37,10 @@ pub struct Sink {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub bluetooth: bool,
|
||||
|
||||
// first channel, in percent
|
||||
pub volume: u8,
|
||||
pub muted: bool,
|
||||
}
|
||||
|
||||
pub fn sink_inputs(text: &str) -> Vec<Stream> {
|
||||
@@ -97,6 +101,10 @@ pub fn sinks(text: &str) -> Vec<Sink> {
|
||||
sink.name = name.to_string();
|
||||
} else if let Some(description) = line.strip_prefix("Description: ") {
|
||||
sink.description = description.to_string();
|
||||
} else if let Some(mute) = line.strip_prefix("Mute: ") {
|
||||
sink.muted = mute == "yes";
|
||||
} else if let Some(volume) = line.strip_prefix("Volume: ") {
|
||||
sink.volume = first_percent(volume);
|
||||
} else if line == "device.api = \"bluez5\"" {
|
||||
sink.bluetooth = true;
|
||||
}
|
||||
@@ -105,6 +113,17 @@ pub fn sinks(text: &str) -> Vec<Sink> {
|
||||
sinks
|
||||
}
|
||||
|
||||
/*
|
||||
* what a person calls an output: the monitor or receiver an hdmi port names in brackets,
|
||||
* "... DisplayPort 1 Output [TX-NR515] (Stereo)" gives "TX-NR515", else the description
|
||||
*/
|
||||
pub fn short_sink_name(description: &str) -> &str {
|
||||
match (description.find('['), description.find(']')) {
|
||||
(Some(open), Some(close)) if open < close => &description[open + 1..close],
|
||||
_ => description,
|
||||
}
|
||||
}
|
||||
|
||||
// "front-left: 52429 / 80% / -5.81 dB, front-right: ..." gives 80
|
||||
fn first_percent(volume: &str) -> u8 {
|
||||
let percent = volume.split('/').nth(1).map(|part| part.trim().trim_end_matches('%'));
|
||||
@@ -462,6 +481,9 @@ Sink Input #57
|
||||
\tState: RUNNING
|
||||
\tName: alsa_output.usb-Yamaha_AG03MK2-00.analog-stereo
|
||||
\tDescription: Yamaha AG03MK2 Analog Stereo
|
||||
\tMute: no
|
||||
\tVolume: front-left: 52428 / 80% / -5.81 dB, front-right: 52428 / 80% / -5.81 dB
|
||||
\tBase Volume: 65536 / 100% / 0.00 dB
|
||||
\tProperties:
|
||||
\t\tdevice.api = \"alsa\"
|
||||
\tPorts:
|
||||
@@ -470,6 +492,7 @@ Sink Input #57
|
||||
Sink #80
|
||||
\tName: bluez_output.00_1B_66_AA_BB_CC.1
|
||||
\tDescription: JBL Tune 720BT
|
||||
\tMute: yes
|
||||
\tProperties:
|
||||
\t\tdevice.api = \"bluez5\"
|
||||
";
|
||||
@@ -492,7 +515,11 @@ Sink #80
|
||||
assert_eq!(sinks.len(), 2);
|
||||
assert_eq!((sinks[0].index, sinks[0].bluetooth), (65, false));
|
||||
assert_eq!(sinks[0].description, "Yamaha AG03MK2 Analog Stereo");
|
||||
assert_eq!((sinks[0].volume, sinks[0].muted, sinks[1].muted), (80, false, true));
|
||||
assert_eq!((sinks[1].name.as_str(), sinks[1].description.as_str(), sinks[1].bluetooth), ("bluez_output.00_1B_66_AA_BB_CC.1", "JBL Tune 720BT", true));
|
||||
|
||||
assert_eq!(short_sink_name("700 Series Chipset Family HD Audio HDMI / DisplayPort 1 Output [TX-NR515] (Stereo)"), "TX-NR515");
|
||||
assert_eq!(short_sink_name("Yamaha AG03MK2 Analog Stereo"), "Yamaha AG03MK2 Analog Stereo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+64
-1
@@ -69,6 +69,9 @@ pub struct Sound {
|
||||
outputs: Vec<Sink>,
|
||||
channels: Vec<Channel>,
|
||||
|
||||
// the sink new sound goes to (`pactl get-default-sink`)
|
||||
default_output: String,
|
||||
|
||||
// by player identity, so "Cider" keeps its curve across restarts
|
||||
eqs: BTreeMap<String, Eq>,
|
||||
unsaved: bool,
|
||||
@@ -108,6 +111,63 @@ impl Sound {
|
||||
&self.outputs
|
||||
}
|
||||
|
||||
pub fn default_output(&self) -> &str {
|
||||
&self.default_output
|
||||
}
|
||||
|
||||
// the first output whose pactl name or description contains `wanted`; none for an empty `wanted`
|
||||
pub fn output_named(&self, wanted: &str) -> Option<&Sink> {
|
||||
let wanted = wanted.to_lowercase();
|
||||
|
||||
(!wanted.is_empty())
|
||||
.then(|| self.outputs.iter().find(|sink| sink.description.to_lowercase().contains(&wanted) || sink.name.to_lowercase().contains(&wanted)))
|
||||
.flatten()
|
||||
}
|
||||
|
||||
// a whole output's volume, unmuting it, as the control centre's sliders set it
|
||||
pub fn set_output_volume(name: &str, volume: u8) {
|
||||
let mut sound = Self::write();
|
||||
|
||||
sound.hold();
|
||||
|
||||
let Some(sink) = sound.outputs.iter_mut().find(|sink| sink.name == name) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if sink.muted {
|
||||
run(&["pactl", "set-sink-mute", name, "0"]);
|
||||
}
|
||||
|
||||
run(&["pactl", "set-sink-volume", name, &format!("{volume}%")]);
|
||||
|
||||
sink.volume = volume;
|
||||
sink.muted = false;
|
||||
}
|
||||
|
||||
pub fn toggle_output_mute(name: &str) {
|
||||
let mut sound = Self::write();
|
||||
|
||||
sound.hold();
|
||||
|
||||
let Some(sink) = sound.outputs.iter_mut().find(|sink| sink.name == name) else {
|
||||
return;
|
||||
};
|
||||
|
||||
sink.muted = !sink.muted;
|
||||
|
||||
run(&["pactl", "set-sink-mute", name, if sink.muted { "1" } else { "0" }]);
|
||||
}
|
||||
|
||||
// where new sound goes; players without an output of their own follow it
|
||||
pub fn set_default_output(name: &str) {
|
||||
let mut sound = Self::write();
|
||||
|
||||
sound.hold();
|
||||
sound.default_output = name.to_string();
|
||||
|
||||
run(&["pactl", "set-default-sink", name]);
|
||||
}
|
||||
|
||||
pub fn channel(&self, player: &str) -> Option<&Channel> {
|
||||
self.channels.iter().find(|channel| channel.player == player)
|
||||
}
|
||||
@@ -351,10 +411,12 @@ fn poll(known: &mut BTreeSet<String>) {
|
||||
|
||||
let outputs: Vec<Sink> = sinks.into_iter().filter(|sink| !sink.name.starts_with(EQ_PREFIX)).collect();
|
||||
|
||||
let default_output = amane::output("pactl get-default-sink");
|
||||
|
||||
let stale = {
|
||||
let sound = Sound::read();
|
||||
|
||||
!sound.held() && (sound.channels != channels || sound.outputs != outputs)
|
||||
!sound.held() && (sound.channels != channels || sound.outputs != outputs || sound.default_output != default_output)
|
||||
};
|
||||
|
||||
if stale {
|
||||
@@ -362,6 +424,7 @@ fn poll(known: &mut BTreeSet<String>) {
|
||||
|
||||
sound.channels = channels;
|
||||
sound.outputs = outputs;
|
||||
sound.default_output = default_output;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+99
-16
@@ -46,6 +46,20 @@ pub struct Wifi {
|
||||
pub hotspot: bool,
|
||||
pub hotspot_name: String,
|
||||
pub clients: usize,
|
||||
|
||||
// the networks in range, the connected one first, then by signal
|
||||
pub networks: Vec<Network>,
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
pub struct Network {
|
||||
// iwd's object path, which Connect is called on
|
||||
pub path: String,
|
||||
pub name: String,
|
||||
pub secured: bool,
|
||||
pub known: bool,
|
||||
pub connected: bool,
|
||||
pub bars: u8,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
@@ -136,21 +150,32 @@ fn wifi() -> Wifi {
|
||||
let connected = station.get("ConnectedNetwork").text();
|
||||
let network = objects.get(connected).map_or("", |network| network.get("net.connman.iwd.Network").get("Name").text());
|
||||
|
||||
// the connected network's signal, in hundredths of a dBm
|
||||
let bars = if connected.is_empty() {
|
||||
0
|
||||
} else {
|
||||
let ordered = bus.call(IWD, path, STATION, "GetOrderedNetworks", &[]);
|
||||
// every network in range as [path, signal in hundredths of a dBm], strongest first
|
||||
let ordered = if matches!(station, Value::Map(_)) { bus.call(IWD, path, STATION, "GetOrderedNetworks", &[]) } else { Value::Nothing };
|
||||
|
||||
let signal = ordered
|
||||
.list()
|
||||
.iter()
|
||||
.find(|entry| entry.list().first().map(|path| path.text()) == Some(connected))
|
||||
.and_then(|entry| entry.list().get(1))
|
||||
.map_or(-10_000.0, |signal| signal.number());
|
||||
let mut networks: Vec<Network> = ordered
|
||||
.list()
|
||||
.iter()
|
||||
.filter_map(|entry| {
|
||||
let path = entry.list().first()?.text();
|
||||
let found = objects.get(path)?.get("net.connman.iwd.Network");
|
||||
let signal = entry.list().get(1).map_or(-10_000.0, |signal| signal.number());
|
||||
|
||||
parse::wifi_bars(signal / 100.0)
|
||||
};
|
||||
Some(Network {
|
||||
path: path.to_string(),
|
||||
name: found.get("Name").text().to_string(),
|
||||
secured: found.get("Type").text() != "open",
|
||||
known: !found.get("KnownNetwork").text().is_empty(),
|
||||
connected: path == connected,
|
||||
bars: parse::wifi_bars(signal / 100.0),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
// a stable sort keeps the signal order within each group
|
||||
networks.sort_by_key(|network| !network.connected);
|
||||
|
||||
let bars = networks.iter().find(|network| network.connected).map_or(0, |network| network.bars);
|
||||
|
||||
let hotspot = access_point.get("Started").bool();
|
||||
|
||||
@@ -171,6 +196,7 @@ fn wifi() -> Wifi {
|
||||
hotspot,
|
||||
hotspot_name: access_point.get("Name").text().to_string(),
|
||||
clients,
|
||||
networks,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,6 +328,33 @@ fn stop_hotspot(device: &str) {
|
||||
bus.set_property(IWD, device, DEVICE, "Mode", Argument::from("station"));
|
||||
}
|
||||
|
||||
// the Wi-Fi menu: joins a known or open network (a new secured one needs a password, which the menu can't ask for yet)
|
||||
pub fn connect_network(path: &str) {
|
||||
Bus::system().call(IWD, path, "net.connman.iwd.Network", "Connect", &[]);
|
||||
}
|
||||
|
||||
pub fn disconnect_wifi() {
|
||||
let device = Links::read().wifi.device.clone();
|
||||
|
||||
if !device.is_empty() {
|
||||
Bus::system().call(IWD, &device, STATION, "Disconnect", &[]);
|
||||
}
|
||||
}
|
||||
|
||||
// a fresh list for the open Wi-Fi menu; iwd answers once the scan is done
|
||||
pub fn scan_wifi() {
|
||||
let wifi = Links::read().wifi.clone();
|
||||
|
||||
if wifi.service && wifi.powered && !wifi.access_point && !wifi.device.is_empty() {
|
||||
Bus::system().call(IWD, &wifi.device, STATION, "Scan", &[]);
|
||||
}
|
||||
}
|
||||
|
||||
// back to how the desktop runs without Wi-Fi: iwd not running at all
|
||||
pub fn stop_wifi_service() {
|
||||
root(&["rc-service", "iwd", "stop"]);
|
||||
}
|
||||
|
||||
pub fn toggle_vpn(service: &str) {
|
||||
if !parse::safe_name(service) {
|
||||
return;
|
||||
@@ -460,19 +513,49 @@ pub fn set_profile(profile: &str) {
|
||||
refresh_power();
|
||||
}
|
||||
|
||||
// logind writes the root-owned file for whoever sits at the machine, as amane's Brightness does
|
||||
/*
|
||||
* the light's file directly when the user may write it (a udev rule makes it 0666 on the
|
||||
* desktop), else logind, which only does it for a session with a seat: a shell started
|
||||
* over ssh has none, which is why logind alone failed
|
||||
*/
|
||||
pub fn set_keyboard(level: u32) {
|
||||
let Some(light) = Power::read().keyboard.clone() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let arguments = [Argument::from("leds"), Argument::from(light.name.as_str()), Argument::from(level.min(light.max))];
|
||||
let level = level.min(light.max);
|
||||
|
||||
Bus::system().call("org.freedesktop.login1", "/org/freedesktop/login1/session/auto", "org.freedesktop.login1.Session", "SetBrightness", &arguments);
|
||||
let file = Path::new("/sys/class/leds").join(&light.name).join("brightness");
|
||||
|
||||
if fs::write(&file, level.to_string()).is_err() {
|
||||
logind_brightness("leds", &light.name, level);
|
||||
}
|
||||
|
||||
refresh_power();
|
||||
}
|
||||
|
||||
// the laptop screen the same way, at 1% or more; amane's Brightness reads it back within half a second
|
||||
pub fn set_screen(percent: u8) {
|
||||
let Some(folder) = fs::read_dir("/sys/class/backlight").ok().and_then(|mut entries| entries.next()).and_then(Result::ok).map(|entry| entry.path()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let highest: u64 = read(&folder.join("max_brightness")).parse().unwrap_or(0);
|
||||
let level = highest * u64::from(percent.clamp(1, 100)) / 100;
|
||||
|
||||
if fs::write(folder.join("brightness"), level.to_string()).is_err() {
|
||||
let name = folder.file_name().map(|name| name.to_string_lossy().into_owned()).unwrap_or_default();
|
||||
|
||||
logind_brightness("backlight", &name, level as u32);
|
||||
}
|
||||
}
|
||||
|
||||
fn logind_brightness(subsystem: &str, name: &str, level: u32) {
|
||||
let arguments = [Argument::from(subsystem), Argument::from(name), Argument::from(level)];
|
||||
|
||||
Bus::system().call("org.freedesktop.login1", "/org/freedesktop/login1/session/auto", "org.freedesktop.login1.Session", "SetBrightness", &arguments);
|
||||
}
|
||||
|
||||
// the external gpu
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
|
||||
@@ -76,6 +76,9 @@ pub struct Theme {
|
||||
|
||||
// the iwd access point profile the control centre's hotspot starts (/var/lib/iwd/ap/<name>.ap); empty: none
|
||||
pub hotspot: String,
|
||||
|
||||
// a second Sound slider in the control centre, for the output whose name contains this; empty: none
|
||||
pub extra_output: String,
|
||||
}
|
||||
|
||||
impl Service for Theme {
|
||||
@@ -84,6 +87,7 @@ impl Service for Theme {
|
||||
colors: Animation::new(PINK_ON_PURPLE),
|
||||
cover_colors: false,
|
||||
hotspot: String::new(),
|
||||
extra_output: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,6 +183,7 @@ fn apply(theme: &str, config: &str, startup: bool) {
|
||||
|
||||
theme.cover_colors = setting("cover_colors") == Some("true");
|
||||
theme.hotspot = setting("hotspot").unwrap_or("").to_string();
|
||||
theme.extra_output = setting("extra_output").unwrap_or("").to_string();
|
||||
|
||||
// a change in the middle of a fade starts from the colours on screen now
|
||||
let now = theme.colors.value();
|
||||
|
||||
Reference in New Issue
Block a user