("profile-id");
128 |
129 | if profile_id.is_empty() {
130 | return None;
131 | }
132 |
133 | Profile::from_id(&profile_id)
134 | .inspect_err(|err| {
135 | tracing::warn!("Failed to get profile with id `{}`: {:?}", profile_id, err);
136 | })
137 | .ok()
138 | .filter(|profile| profile.is_available())
139 | }
140 |
141 | pub fn connect_profile_changed(&self, f: impl Fn(&Self) + 'static) -> glib::SignalHandlerId {
142 | self.0
143 | .connect_changed(Some("profile-id"), move |settings, _| {
144 | f(&Self(settings.clone()));
145 | })
146 | }
147 |
148 | pub fn reset_profile(&self) {
149 | self.0.reset("profile-id");
150 | }
151 | }
152 |
153 | #[cfg(test)]
154 | mod tests {
155 | use super::*;
156 |
157 | use std::{env, process::Command, sync::Once};
158 |
159 | fn setup_schema() {
160 | static INIT: Once = Once::new();
161 |
162 | INIT.call_once(|| {
163 | let schema_dir = concat!(env!("CARGO_MANIFEST_DIR"), "/data");
164 |
165 | let output = Command::new("glib-compile-schemas")
166 | .arg(schema_dir)
167 | .output()
168 | .unwrap();
169 |
170 | if !output.status.success() {
171 | panic!(
172 | "Failed to compile GSchema for tests; stdout: {}; stderr: {}",
173 | String::from_utf8_lossy(&output.stdout),
174 | String::from_utf8_lossy(&output.stderr)
175 | );
176 | }
177 |
178 | unsafe {
179 | env::set_var("GSETTINGS_SCHEMA_DIR", schema_dir);
180 | env::set_var("GSETTINGS_BACKEND", "memory");
181 | }
182 | });
183 | }
184 |
185 | #[test]
186 | fn default_profile() {
187 | setup_schema();
188 | gst::init().unwrap();
189 |
190 | assert!(Settings::default().profile().is_some());
191 | assert!(Settings::default().profile().unwrap().supports_audio());
192 | }
193 | }
194 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Kooha
5 |
6 |
7 |
8 | Elegantly record your screen
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 | Capture your screen in an intuitive and straightforward way without distractions.
40 |
41 | Kooha is a simple screen recorder with a minimal interface. You can simply click
42 | the record button without having to configure a bunch of settings.
43 |
44 | The main features of Kooha include the following:
45 | * 🎙️ Record microphone, desktop audio, or both at the same time
46 | * 📼 Support for WebM, MP4, GIF, and Matroska formats
47 | * 🖥️ Select a monitor or a portion of the screen to record
48 | * 🛠️ Configurable saving location, pointer visibility, frame rate, and delay
49 | * 🚀 Experimental hardware-accelerated encoding
50 |
51 | ## 😕 It Doesn't Work
52 |
53 | There are many possibilities on why it may not be working. You may not have
54 | the runtime requirements mentioned below installed, or your distro doesn't
55 | support it. For troubleshooting purposes, the [screencast compatibility page](https://github.com/emersion/xdg-desktop-portal-wlr/wiki/Screencast-Compatibility)
56 | of `xdg-desktop-portal-wlr` wiki may help determine if your distro
57 | has support for it out of the box. If it does, but it still doesn't work, you
58 | can also check for the [troubleshooting checklist](https://github.com/emersion/xdg-desktop-portal-wlr/wiki/%22It-doesn't-work%22-Troubleshooting-Checklist).
59 |
60 | ## ⚙️ Experimental Features
61 |
62 | These features are disabled by default due to stability issues and possible
63 | performance degradation. However, they can be enabled manually by running Kooha
64 | with `KOOHA_EXPERIMENTAL` env var set to `all` (e.g., `KOOHA_EXPERIMENTAL=all flatpak run io.github.seadve.Kooha`), or individually, by setting
65 | `KOOHA_EXPERIMENTAL` to the following keys (e.g., `KOOHA_EXPERIMENTAL=experimental-formats,window-recording`):
66 |
67 | | Feature | Description | Issues |
68 | | ------------------------ | ----------------------------------------------------------------------- | ------------------------- |
69 | | `all` | Enables all experimental features | - |
70 | | `experimental-formats` | Enables other codecs (e.g., hardware-accelerate encoders, VP9, and AV1) | Stability |
71 | | `multiple-video-sources` | Enables recording multiple monitor or windows | Stability and performance |
72 | | `window-recording` | Enables recording a specific window | Flickering |
73 |
74 | ## 📋 Runtime Requirements
75 |
76 | * pipewire
77 | * gstreamer-plugin-pipewire
78 | * xdg-desktop-portal
79 | * xdg-desktop-portal-(e.g., gtk, kde, wlr)
80 |
81 | ## 🏗️ Building from source
82 |
83 | ### GNOME Builder
84 |
85 | GNOME Builder is the environment used for developing this application.
86 | It can use Flatpak manifests to create a consistent building and running
87 | environment cross-distro. Thus, it is highly recommended you use it.
88 |
89 | 1. Download [GNOME Builder](https://flathub.org/apps/details/org.gnome.Builder).
90 | 2. In Builder, click the "Clone Repository" button at the bottom, using `https://github.com/SeaDve/Kooha.git` as the URL.
91 | 3. Click the build button at the top once the project is loaded.
92 |
93 | ### Meson
94 |
95 | #### Prerequisites
96 |
97 | The following packages are required to build Kooha:
98 |
99 | * meson
100 | * ninja
101 | * appstreamcli (for checks)
102 | * cargo
103 | * x264 (for MP4)
104 | * gstreamer
105 | * gstreamer-plugins-base
106 | * gstreamer-plugins-ugly (for MP4)
107 | * gstreamer-plugins-bad (for VA encoders)
108 | * glib2
109 | * gtk4
110 | * libadwaita
111 |
112 | #### Build Instruction
113 |
114 | ```shell
115 | git clone https://github.com/SeaDve/Kooha.git
116 | cd Kooha
117 | meson _build --prefix=/usr/local
118 | ninja -C _build install
119 | ```
120 |
121 | ## 📦 Third-Party Packages
122 |
123 | Unlike Flatpak, take note that these packages are not officially supported by the developer.
124 |
125 | ### Repology
126 |
127 | You can also check out other third-party packages on [Repology](https://repology.org/project/kooha/versions).
128 |
129 | ## 🙌 Help translate Kooha
130 |
131 | You can help Kooha translate into your native language. If you find any typos
132 | or think you can improve a translation, you can use the [Weblate](https://hosted.weblate.org/engage/seadve/) platform.
133 |
134 | ## ☕ Support me and the project
135 |
136 | Kooha is free and will always be for everyone to use. If you like the project and
137 | would like to support it, you may donate [here](https://seadve.github.io/donate/).
138 |
139 | ## 💝 Acknowledgment
140 |
141 | I would like to express my gratitude to the [contributors](https://github.com/SeaDve/Kooha/graphs/contributors)
142 | and [translators](https://hosted.weblate.org/engage/seadve/) of the project.
143 |
144 | I would also like to thank the open-source software projects, libraries, and APIs that were
145 | used in developing this app, such as GStreamer, GTK, LibAdwaita, and many others, for making Kooha possible.
146 |
147 | I would also like to acknowledge [RecApp](https://github.com/amikha1lov/RecApp), which greatly inspired the creation of Kooha,
148 | as well as [GNOME Screenshot](https://gitlab.gnome.org/GNOME/gnome-screenshot), which served as a reference for Kooha's icon
149 | design.
150 |
--------------------------------------------------------------------------------
/src/about.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | env,
3 | fs::File,
4 | io::{BufRead, BufReader},
5 | path::Path,
6 | process::{Command, Stdio},
7 | };
8 |
9 | use adw::prelude::*;
10 | use anyhow::Result;
11 | use anyhow::anyhow;
12 | use gettextrs::gettext;
13 | use gst::prelude::*;
14 | use gtk::glib;
15 |
16 | use crate::{
17 | config::{APP_ID, VERSION},
18 | experimental,
19 | };
20 |
21 | pub fn present_dialog(parent: &impl IsA) {
22 | let dialog = adw::AboutDialog::builder()
23 | .application_icon(APP_ID)
24 | .application_name(gettext("Kooha"))
25 | .developer_name("Dave Patrick Caberto")
26 | .version(VERSION)
27 | .copyright("© 2024 Dave Patrick Caberto")
28 | .license_type(gtk::License::Gpl30)
29 | .developers(vec![
30 | "Dave Patrick Caberto",
31 | "Mathiascode",
32 | "Felix Weilbach",
33 | ])
34 | // Translators: Replace "translator-credits" with your names. Put a comma between.
35 | .translator_credits(gettext("translator-credits"))
36 | .issue_url("https://github.com/SeaDve/Kooha/issues")
37 | .support_url("https://github.com/SeaDve/Kooha/discussions")
38 | .debug_info(debug_info())
39 | .debug_info_filename("kooha-debug-info")
40 | .release_notes_version("2.3.0")
41 | .release_notes(release_notes())
42 | .build();
43 |
44 | dialog.add_link(&gettext("Donate"), "https://seadve.github.io/donate/");
45 | dialog.add_link(&gettext("GitHub"), "https://github.com/SeaDve/Kooha");
46 | dialog.add_link(
47 | &gettext("Translate"),
48 | "https://hosted.weblate.org/projects/seadve/kooha",
49 | );
50 |
51 | dialog.present(Some(parent));
52 | }
53 |
54 | fn release_notes() -> &'static str {
55 | r#"This release contains new features and fixes:
56 |
57 | - Area selector window is now resizable
58 | - Previous selected area is now remembered
59 | - Logout and idle are now inhibited while recording
60 | - Video format and FPS are now shown in the main view
61 | - Notifications now show the duration and size of the recording
62 | - Notification actions now work even when the application is closed
63 | - Progress is now shown when flushing the recording
64 | - It is now much easier to pick from frame rate options
65 | - Actually fixed audio from stuttering and being cut on long recordings
66 | - Record audio in stereo rather than mono when possible
67 | - Recordings are no longer deleted when flushing is cancelled
68 | - Significant improvements in recording performance
69 | - Improved preferences dialog UI
70 | - Fixed incorrect output video orientation on certain compositors
71 | - Fixed incorrect focus on area selector
72 | - Fixed too small area selector window default size on HiDPI monitors
73 | - Updated translations
74 |
"#
75 | }
76 |
77 | fn cpu_model() -> Result {
78 | let output = Command::new("lscpu")
79 | .stdout(Stdio::piped())
80 | .spawn()?
81 | .wait_with_output()?;
82 |
83 | for res in output.stdout.lines() {
84 | let line = res?;
85 |
86 | if line.contains("Model name:")
87 | && let Some((_, value)) = line.split_once(':')
88 | {
89 | return Ok(value.trim().to_string());
90 | }
91 | }
92 |
93 | Ok("".into())
94 | }
95 |
96 | fn gpu_model() -> Result {
97 | let output = Command::new("lspci")
98 | .stdout(Stdio::piped())
99 | .spawn()?
100 | .wait_with_output()?;
101 |
102 | for res in output.stdout.lines() {
103 | let line = res?;
104 |
105 | if line.contains("VGA")
106 | && let Some(value) = line.splitn(3, ':').last()
107 | {
108 | return Ok(value.trim().to_string());
109 | }
110 | }
111 |
112 | Ok("".into())
113 | }
114 |
115 | fn is_flatpak() -> bool {
116 | Path::new("/.flatpak-info").exists()
117 | }
118 |
119 | fn debug_info() -> String {
120 | let container = env::var("container")
121 | .ok()
122 | .or_else(|| is_flatpak().then(|| "Flatpak".to_string()))
123 | .unwrap_or_else(|| "none".into());
124 | let experimental_features = experimental::enabled_features();
125 |
126 | let language_names = glib::language_names().join(", ");
127 |
128 | let cpu_model = cpu_model().unwrap_or_else(|e| format!("<{}>", e));
129 | let gpu_model = gpu_model().unwrap_or_else(|e| format!("<{}>", e));
130 |
131 | let distribution = os_info("PRETTY_NAME").unwrap_or_else(|e| format!("<{}>", e));
132 | let desktop_session = env::var("DESKTOP_SESSION").unwrap_or_else(|_| "".into());
133 | let display_server = env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "".into());
134 |
135 | let gtk_version = format!(
136 | "{}.{}.{}",
137 | gtk::major_version(),
138 | gtk::minor_version(),
139 | gtk::micro_version()
140 | );
141 | let adw_version = format!(
142 | "{}.{}.{}",
143 | adw::major_version(),
144 | adw::minor_version(),
145 | adw::micro_version()
146 | );
147 | let gst_version_string = gst::version_string();
148 | let pipewire_version = gst::Registry::get()
149 | .find_feature("pipewiresrc", gst::ElementFactory::static_type())
150 | .map_or("".into(), |feature| {
151 | feature
152 | .plugin()
153 | .map_or("".into(), |plugin| plugin.version())
154 | });
155 |
156 | format!(
157 | r#"- {APP_ID} {VERSION}
158 | - Container: {container}
159 | - Experimental Features: {experimental_features:?}
160 |
161 | - Language: {language_names}
162 |
163 | - CPU: {cpu_model}
164 | - GPU: {gpu_model}
165 |
166 | - Distribution: {distribution}
167 | - Desktop Session: {desktop_session}
168 | - Display Server: {display_server}
169 |
170 | - GTK {gtk_version}
171 | - Libadwaita {adw_version}
172 | - {gst_version_string}
173 | - Pipewire {pipewire_version}"#
174 | )
175 | }
176 |
177 | fn os_info(key_name: &str) -> Result {
178 | let os_release_path = if is_flatpak() {
179 | "/run/host/etc/os-release"
180 | } else {
181 | "/etc/os-release"
182 | };
183 | let file = File::open(os_release_path)?;
184 |
185 | for line in BufReader::new(file).lines() {
186 | let line = line?;
187 | let Some((key, value)) = line.split_once('=') else {
188 | continue;
189 | };
190 |
191 | if key == key_name {
192 | return Ok(value.trim_matches('\"').to_string());
193 | }
194 | }
195 |
196 | Err(anyhow!("unknown"))
197 | }
198 |
--------------------------------------------------------------------------------
/src/timer.rs:
--------------------------------------------------------------------------------
1 | use futures_util::future::FusedFuture;
2 | use gtk::glib::{self, clone};
3 |
4 | use std::{
5 | cell::{Cell, RefCell},
6 | fmt,
7 | future::Future,
8 | pin::Pin,
9 | rc::Rc,
10 | task::{Context, Poll, Waker},
11 | time::{Duration, Instant},
12 | };
13 |
14 | use crate::cancelled::Cancelled;
15 |
16 | const SECS_LEFT_UPDATE_INTERVAL: Duration = Duration::from_millis(200);
17 |
18 | /// A reference counted cancellable timed future
19 | ///
20 | /// The timer will only start when it gets polled.
21 | #[derive(Clone)]
22 | pub struct Timer {
23 | inner: Rc,
24 | }
25 |
26 | impl fmt::Debug for Timer {
27 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28 | f.debug_struct("Timer")
29 | .field("duration", &self.inner.duration)
30 | .field("state", &self.inner.state.get())
31 | .field("elapsed", &self.inner.instant.get().map(|i| i.elapsed()))
32 | .finish()
33 | }
34 | }
35 |
36 | #[derive(Debug, Clone, Copy)]
37 | enum State {
38 | Waiting,
39 | Cancelled,
40 | Done,
41 | }
42 |
43 | impl State {
44 | fn to_poll(self) -> Poll<::Output> {
45 | match self {
46 | State::Waiting => Poll::Pending,
47 | State::Cancelled => Poll::Ready(Err(Cancelled::new("timer"))),
48 | State::Done => Poll::Ready(Ok(())),
49 | }
50 | }
51 | }
52 |
53 | struct Inner {
54 | duration: Duration,
55 |
56 | secs_left_changed_cb: Box,
57 | secs_left_changed_source_id: RefCell