Initial commit

This commit is contained in:
2026-03-30 08:49:04 +09:00
commit 5edd98f9b1
5 changed files with 292 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
target/

12
Cargo.toml Normal file
View File

@@ -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"] }

138
src/main.rs Normal file
View File

@@ -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<String> = 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)
}
}
}
}

13
src/notification.rs Normal file
View File

@@ -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();
}

128
src/password.rs Normal file
View File

@@ -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<Cell<bool>>) {
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();
});
}