From 5edd98f9b11452509ddae9d0f4739add06bf8721 Mon Sep 17 00:00:00 2001 From: Morgan Date: Mon, 30 Mar 2026 08:49:04 +0900 Subject: [PATCH] Initial commit --- .gitignore | 1 + Cargo.toml | 12 ++++ src/main.rs | 138 ++++++++++++++++++++++++++++++++++++++++++++ src/notification.rs | 13 +++++ src/password.rs | 128 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 292 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/main.rs create mode 100644 src/notification.rs create mode 100644 src/password.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9f97022 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +target/ \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ea92cd3 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "adw-askpass" +version = "0.0.1" +edition = "2024" + +[[bin]] +name = "adw-askpass" +path = "src/main.rs" + +[dependencies] +gtk4 = "0.11" +libadwaita = { version = "0.9", features = ["v1_2"] } diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2ef5e5f --- /dev/null +++ b/src/main.rs @@ -0,0 +1,138 @@ +mod notification; +mod password; + +use std::cell::Cell; +use std::env; +use std::process::ExitCode; +use std::rc::Rc; + +use gtk4::prelude::*; +use libadwaita as adw; + +enum Mode { + Password, + Notification, +} + +pub struct Args { + mode: Mode, + title: String, + message: String, + icon: String, + ok_label: String, + cancel_label: String, + timeout: u32, +} + +impl Default for Args { + fn default() -> Self { + Self { + mode: Mode::Password, + title: "Authentication Required".into(), + message: "Enter your password to continue".into(), + icon: "dialog-password-symbolic".into(), + ok_label: "Unlock".into(), + cancel_label: "Cancel".into(), + timeout: 0, + } + } +} + +impl Args { + fn parse() -> Self { + let mut args = Args::default(); + let raw: Vec = env::args().skip(1).collect(); + let mut i = 0; + + while i < raw.len() { + let a = &raw[i]; + + if let Some((key, val)) = a.split_once('=') { + match key { + "--title" => args.title = val.to_string(), + "--text" | "--message" => args.message = val.to_string(), + "--icon" | "--window-icon" => args.icon = val.to_string(), + "--ok-label" => args.ok_label = val.to_string(), + "--cancel-label" => args.cancel_label = val.to_string(), + "--timeout" => args.timeout = val.parse().unwrap_or(0), + _ => {} + } + i += 1; + continue; + } + + match a.as_str() { + "--password" | "--modal" => { + i += 1; + continue; + } + "--notification" => { + args.mode = Mode::Notification; + if args.icon == "dialog-password-symbolic" { + args.icon = "dialog-information-symbolic".into(); + } + i += 1; + continue; + } + _ => {} + } + + let next = raw.get(i + 1).map(|s| s.as_str()); + let consumed = match a.as_str() { + "--title" => { + if let Some(v) = next { args.title = v.to_string(); true } else { false } + } + "--text" | "--message" => { + if let Some(v) = next { args.message = v.to_string(); true } else { false } + } + "--icon" | "--window-icon" => { + if let Some(v) = next { args.icon = v.to_string(); true } else { false } + } + "--ok-label" => { + if let Some(v) = next { args.ok_label = v.to_string(); true } else { false } + } + "--cancel-label" => { + if let Some(v) = next { args.cancel_label = v.to_string(); true } else { false } + } + "--timeout" => { + if let Some(v) = next { args.timeout = v.parse().unwrap_or(0); true } else { false } + } + _ => false, + }; + + if !consumed && !a.starts_with('-') { + args.message = a.to_string(); + } + + i += if consumed { 2 } else { 1 }; + } + + args + } +} + +fn main() -> ExitCode { + let args = Args::parse(); + let submitted = Rc::new(Cell::new(false)); + + let app = adw::Application::new(None, gtk4::gio::ApplicationFlags::default()); + + match args.mode { + Mode::Notification => { + notification::send(&args); + ExitCode::SUCCESS + } + Mode::Password => { + let submitted_clone = submitted.clone(); + app.connect_activate(move |app| { + password::build(app, &args, &submitted_clone); + }); + app.run_with_args::<&str>(&[]); + if submitted.get() { + ExitCode::SUCCESS + } else { + ExitCode::from(1) + } + } + } +} diff --git a/src/notification.rs b/src/notification.rs new file mode 100644 index 0000000..45451b6 --- /dev/null +++ b/src/notification.rs @@ -0,0 +1,13 @@ +use std::process::Command; + +use crate::Args; + +pub fn send(args: &Args) { + let _ = Command::new("notify-send") + .arg("--icon") + .arg(&args.icon) + .arg("--app-name") + .arg(&args.title) + .arg(&args.message) + .spawn(); +} diff --git a/src/password.rs b/src/password.rs new file mode 100644 index 0000000..79d3fe7 --- /dev/null +++ b/src/password.rs @@ -0,0 +1,128 @@ +use std::cell::Cell; +use std::io::Write; +use std::rc::Rc; + +use gtk4 as gtk; +use gtk4::glib; +use gtk4::prelude::*; +use libadwaita as adw; +use libadwaita::prelude::*; + +use crate::Args; + +pub fn build(app: &adw::Application, args: &Args, submitted: &Rc>) { + let window = adw::ApplicationWindow::builder() + .application(app) + .default_width(340) + .resizable(false) + .build(); + + // Close on Escape + let esc = gtk::ShortcutController::new(); + let win_ref = window.clone(); + esc.add_shortcut(gtk::Shortcut::new( + gtk::ShortcutTrigger::parse_string("Escape"), + Some(gtk::CallbackAction::new(move |_, _| { + win_ref.close(); + glib::Propagation::Stop + })), + )); + window.add_controller(esc); + + let vbox = gtk::Box::builder() + .orientation(gtk::Orientation::Vertical) + .spacing(14) + .margin_top(32) + .margin_bottom(24) + .margin_start(28) + .margin_end(28) + .build(); + window.set_content(Some(&vbox)); + + let icon = gtk::Image::builder() + .icon_name(&args.icon) + .pixel_size(48) + .margin_bottom(4) + .css_classes(["dim-label"]) + .build(); + vbox.append(&icon); + + let title_label = gtk::Label::builder() + .label(&args.title) + .css_classes(["title-4"]) + .build(); + vbox.append(&title_label); + + let msg_label = gtk::Label::builder() + .label(&args.message) + .css_classes(["dim-label", "body"]) + .wrap(true) + .margin_bottom(4) + .justify(gtk::Justification::Center) + .build(); + vbox.append(&msg_label); + + let list_box = gtk::ListBox::builder() + .selection_mode(gtk::SelectionMode::None) + .css_classes(["boxed-list"]) + .margin_bottom(4) + .build(); + vbox.append(&list_box); + + let entry = adw::PasswordEntryRow::builder().title("Password").build(); + list_box.append(&entry); + + let btn_box = gtk::Box::builder() + .orientation(gtk::Orientation::Horizontal) + .spacing(12) + .homogeneous(true) + .build(); + vbox.append(&btn_box); + + let cancel_btn = gtk::Button::builder() + .label(&args.cancel_label) + .css_classes(["pill"]) + .build(); + let win_ref = window.clone(); + cancel_btn.connect_clicked(move |_| win_ref.close()); + btn_box.append(&cancel_btn); + + let ok_btn = gtk::Button::builder() + .label(&args.ok_label) + .css_classes(["pill", "suggested-action"]) + .build(); + btn_box.append(&ok_btn); + + let submit = { + let entry = entry.clone(); + let window = window.clone(); + let submitted = submitted.clone(); + move || { + let pw: String = entry.text().into(); + if !pw.is_empty() { + let _ = std::io::stdout().write_all(pw.as_bytes()); + let _ = std::io::stdout().flush(); + submitted.set(true); + window.close(); + } + } + }; + + let submit_clone = submit.clone(); + ok_btn.connect_clicked(move |_| submit_clone()); + entry.connect_entry_activated(move |_| submit()); + + if args.timeout > 0 { + let win_ref = window.clone(); + glib::timeout_add_seconds_local_once(args.timeout, move || { + win_ref.close(); + }); + } + + window.present(); + + let entry_ref = entry.clone(); + glib::idle_add_local_once(move || { + entry_ref.grab_focus(); + }); +}