Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

28 changes: 19 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,30 @@ version = "0.1.0"
edition = "2024"

[dependencies]
ashpd = { version = "0.12", default-features = false, features = ["tokio"] }
chrono = { version = "0.4.41", default-features = false, features = [
"alloc",
"clock",
# App
chrono = { version = "0.4", default-features = false, features = [
"alloc",
"clock",
] }
clap = { version = "4.5", features = ["derive"] }
dirs = "6"
tokio = { version = "1.47.1", default-features = false, features = ["macros"] }
clap = { version = "4.5.46", features = ["derive"] }
zbus = { version = "5", default-features = false }
tokio = { version = "1", default-features = false, features = ["macros"] }

# Screenshots/D-Bus comm
ashpd = { version = "0.12", default-features = false, features = [
"tokio",
"tracing",
] }
zbus = { version = "5", default-features = false, features = ["tokio"] }

# Logging
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }

# Internationalization
i18n-embed = { version = "0.16", features = [
"fluent-system",
"desktop-requester",
"fluent-system",
"desktop-requester",
] }
i18n-embed-fl = "0.10"
rust-embed = "8.5"
Expand Down
8 changes: 7 additions & 1 deletion i18n/en/cosmic_screenshot.ftl
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
cosmic-screenshot = COSMIC Screenshot

screenshot-saved-to-clipboard = Screenshot saved to clipboard
screenshot-saved-to = Screenshot saved to:
screenshot-saved-to = Screenshot saved to:

screenshot-cancelled = Screenshot canceled
screenshot-dbus-err = Error from D-Bus
screenshot-failed = Screenshot failed
screenshot-no-dir = Unable to write screenshot to directory: {$path}
screenshot-not-allowed = Screenshot not allowed: {$msg}
126 changes: 126 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
use std::{
error::Error as StdError,
fmt::{self, Display},
io,
path::{Path, PathBuf},
};

use ashpd::{Error as AshpdError, PortalError, desktop::ResponseError};
use zbus::Error as ZbusError;

use crate::fl;

/// Error type for requesting screenshots from the XDG portal.
///
/// The primary purpose of this type is to provide simple user facing messages.
#[derive(Debug)]
pub enum Error {
/// Screenshot errors from the portal or D-Bus
Ashpd(AshpdError),
/// Failure to post a notification
Notify(ZbusError),
/// Invalid directory path passed AND no Pictures XDG directory
MissingSaveDirectory(Option<PathBuf>),
/// Screenshot succeeded but cannot be saved
SaveScreenshot {
error: io::Error,
context: &'static str,
},
}

impl StdError for Error {}

// Log facing display messages for programmers or debugging
impl Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ashpd(e) => e.fmt(f),
Self::Notify(e) => write!(f, "posting a notification: {e}"),
Self::MissingSaveDirectory(p) => {
let msg = p
.as_deref()
.map(|path| format!("opening `{}` or the Pictures directory", path.display()));

write!(
f,
"{}",
msg.as_deref().unwrap_or("opening Pictures directory")
)
}
Self::SaveScreenshot { error, context } => write!(f, "{context}: {error}"),
}
}
}

impl Error {
/// Localized, condensed error message for end users
pub fn to_user_facing(&self) -> String {
match self {
// _ if self.unsupported() => fl!("screenshot-unsupported"),
_ if self.cancelled() => fl!("screenshot-cancelled"),
_ if self.zbus() => fl!("screenshot-dbus-err"),
Self::MissingSaveDirectory(p) => {
fl!(
"screenshot-no-dir",
path = p.as_deref().unwrap_or(Path::new("")).to_string_lossy()
)
}
Self::Ashpd(AshpdError::Portal(PortalError::NotAllowed(msg))) => {
fl!("screenshot-not-allowed", msg = msg)
}
// Self::SaveScreenshot { .. } => "Screenshot captured but couldn't be saved".into(),
_ => fl!("screenshot-failed"),
}
}

/// Screenshot request cancelled
pub fn cancelled(&self) -> bool {
let Self::Ashpd(e) = self else {
return false;
};

match e {
AshpdError::Response(e) => *e == ResponseError::Cancelled,
AshpdError::Portal(PortalError::Cancelled(_)) => true,
_ => false,
}
}

/// Portal does not support screenshots
pub fn unsupported(&self) -> bool {
if let Self::Ashpd(e) = self {
match e {
// Requires version `x` but interface only supports version `y`
AshpdError::RequiresVersion(_, _) => true,
// Unsupported screenshot method or interface for screenshots not found
AshpdError::Portal(PortalError::ZBus(e)) => {
*e == ZbusError::Unsupported || *e == ZbusError::InterfaceNotFound
}
AshpdError::Zbus(e) => {
*e == ZbusError::Unsupported || *e == ZbusError::InterfaceNotFound
}
_ => false,
}
} else {
false
}
}

/// D-Bus communication problem
///
/// [zbus::Error] encapsulates many different problems, many of which are programmer errors
/// which shouldn't occur during normal operation.
pub fn zbus(&self) -> bool {
matches!(
self,
Self::Ashpd(AshpdError::Zbus(_))
| Self::Ashpd(AshpdError::Portal(PortalError::ZBus(_)))
)
}
}

impl From<AshpdError> for Error {
fn from(value: AshpdError) -> Self {
Self::Ashpd(value)
}
}
Loading