Streamline recording controls and compact the display picker

This commit is contained in:
Samuel Onoja committed 2026-09-28 08:45:50 +01:00
1 parent 6209d556ee
commit 299f60e671
11 files changed
+189 -105

No files matched your search

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Cancel display selection"><circle cx="30" cy="30" r="22" fill="none" stroke="currentColor" stroke-width="3"/><path d="m23 23 14 14m0-14L23 37" fill="none" stroke="#3d7be6" stroke-width="3" stroke-linecap="round"/></svg>

After

Width:  |  Height:  |  Size: 303 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Saving recording"><circle cx="30" cy="30" r="22" fill="none" stroke="currentColor" stroke-width="3"/><g fill="#3d7be6"><circle cx="20" cy="30" r="3"/><circle cx="30" cy="30" r="3"/><circle cx="40" cy="30" r="3"/></g></svg>

After

Width:  |  Height:  |  Size: 306 B

@@ -1,5 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Stop recording">
<rect x="8" y="10" width="44" height="32" rx="7" fill="none" stroke="currentColor" stroke-width="3"/>
<path d="M23 50h14M30 42v8" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round"/>
<rect x="22" y="18" width="16" height="16" rx="3" fill="#3d7be6"/>
<circle cx="30" cy="30" r="22" fill="none" stroke="currentColor" stroke-width="3"/>
<rect x="20" y="20" width="20" height="20" rx="4" fill="#3d7be6"/>
</svg>

Before

Width:  |  Height:  |  Size: 380 B

After

Width:  |  Height:  |  Size: 258 B

+54 -2
View File
@@ -36,6 +36,15 @@ pub struct Recorder {
updates: Option<Receiver<Event>>,
}
impl Recorder {
pub fn label(&self) -> &'static str {
match self.state {
State::Selecting => "Cancel display selection",
State::Recording(_) => "Stop recording",
State::Saving => "Saving recording",
_ => "Record a display",
}
}
pub fn busy(&self) -> bool {
self.updates.is_some()
}
@@ -87,11 +96,15 @@ impl Recorder {
let _ = send.send(Event::Exited);
});
}
Err(error) => self.state = State::Error(format!("Cannot start recorder: {error}")),
Err(error) => {
let message = format!("Cannot start recorder: {error}");
notify("Recording unavailable", message.clone());
self.state = State::Error(message);
}
}
}
pub fn stop(&mut self) {
if self.busy() {
if self.busy() && !matches!(self.state, State::Saving) {
// EOF requests EOS/finalization. Never terminate the encoder abruptly.
self.control.take();
self.state = State::Saving;
@@ -134,8 +147,47 @@ impl Recorder {
}
}
if done {
match &self.state {
State::Saved(path) => notify("Recording saved", path.display().to_string()),
State::Error(message) => notify("Recording failed", message.clone()),
_ => (),
}
self.control.take();
self.updates.take();
}
}
}
// Use the normal notification center for results, without an extra recording menu.
fn notify(title: &'static str, body: String) {
std::thread::spawn(move || {
let result = (|| -> zbus::Result<()> {
let connection = zbus::blocking::connection::Builder::session()?
.method_timeout(std::time::Duration::from_secs(3))
.build()?;
let proxy = zbus::blocking::Proxy::new(
&connection,
"org.freedesktop.Notifications",
"/org/freedesktop/Notifications",
"org.freedesktop.Notifications",
)?;
let _: u32 = proxy.call(
"Notify",
&(
"Ferese",
0u32,
"ferese",
title,
body,
Vec::<String>::new(),
std::collections::HashMap::<String, zbus::zvariant::OwnedValue>::new(),
-1i32,
),
)?;
Ok(())
})();
if let Err(error) = result {
eprintln!("ferese-shell: recording notification: {error}");
}
});
}
+26 -45
View File
@@ -257,15 +257,26 @@ impl FereseShell {
if !kind.available(&self.status) {
continue;
}
let (source, enabled) = if kind == Menu::Recording && self.recorder.busy() {
(
include_bytes!("../assets/icons/status/record-stop.svg").as_slice(),
true,
)
let (source, enabled) = if kind == Menu::Recording {
let source: &'static [u8] = match self.recorder.state {
recording::State::Selecting => {
include_bytes!("../assets/icons/status/record-cancel.svg")
}
recording::State::Recording(_) => {
include_bytes!("../assets/icons/status/record-stop.svg")
}
recording::State::Saving => {
include_bytes!("../assets/icons/status/record-saving.svg")
}
_ => include_bytes!("../assets/icons/status/record.svg"),
};
(source, true)
} else {
status_icon(kind, &self.status)
};
let selected = self.menu.as_ref().is_some_and(|m| m.kind == kind);
let selected = self.menu.as_ref().is_some_and(|m| m.kind == kind)
|| (kind == Menu::Recording
&& matches!(self.recorder.state, recording::State::Recording(_)));
let foreground = color(if selected {
theme.accent
} else if enabled {
@@ -326,7 +337,9 @@ impl FereseShell {
let recording_busy = kind == Menu::Recording && self.recorder.busy();
let control = button::custom(content)
.name(if recording_busy {
"Stop screen recording".into()
self.recorder.label().into()
} else if kind == Menu::Recording {
"Record a display".into()
} else {
status_label(kind, &self.status)
})
@@ -336,8 +349,12 @@ impl FereseShell {
])
.height(metrics.control_height)
.on_press_with_rectangle(move |offset, bounds| {
if recording_busy {
return cosmic::Action::App(Message::StopRecording);
if kind == Menu::Recording {
return cosmic::Action::App(if recording_busy {
Message::StopRecording
} else {
Message::StartRecording
});
}
cosmic::Action::App(Message::OpenMenu(
kind,
@@ -434,42 +451,6 @@ impl FereseShell {
]
.spacing(8),
);
} else if kind == Menu::Recording {
use recording::State;
let description = match &self.recorder.state {
State::Idle => {
"Record a display. Video is saved to Videos/Ferese as WebM. Audio is off."
.to_owned()
}
State::Selecting => "Choose a display in the sharing dialog.".to_owned(),
State::Recording(_) => format!(
"Recording {} · audio off",
self.recorder.elapsed().unwrap_or_default()
),
State::Saving => "Finishing your recording…".to_owned(),
State::Saved(path) => format!("Saved to {}", path.display()),
State::Error(message) => message.clone(),
};
rows = rows.push(text(description).size(13).class(theme::Text::Color(muted)));
if !self.recorder.busy() {
rows = rows.push(menu_button(
"Record display",
Message::StartRecording,
primary,
p,
));
} else if !matches!(self.recorder.state, State::Saving) {
rows = rows.push(menu_button(
if matches!(self.recorder.state, State::Selecting) {
"Cancel"
} else {
"Stop and save"
},
Message::StopRecording,
primary,
p,
));
}
} else {
let combined = menu.kind == Menu::System;
if combined {