mod config;
mod text_clipboard;
use crate::text_clipboard::TextClipboard;
use config::Alias;
use config::Config;
use gdk::Key;
use gdk4 as gdk;
use gdk4::glib::Type;
use gio::prelude::*;
use glib::Propagation;
use gtk::prelude::*;
use gtk4 as gtk;
use rayon::prelude::*;
use std::os::unix::net::{UnixListener, UnixStream};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use sublime_fuzzy::FuzzySearch;
const WINDOW_SCREEN_WIDTH_FRACTION: f64 = 0.15;
const WINDOW_SCREEN_HEIGHT_FRACTION: f64 = 0.30;
#[derive(Debug, thiserror::Error)]
enum Error {
#[error("config error: {0}")]
Config(#[from] config::ConfigError),
#[error("runtime directory not available")]
NoRuntimeDir,
#[error("socket error: {0}")]
Socket(std::io::Error),
#[error("UI widget '{0}' not found")]
MissingWidget(&'static str),
#[error("no default display available")]
NoDisplay,
#[error("tree view has no model")]
NoTreeViewModel,
#[error("failed to read string value from model")]
ModelStringRead,
}
fn paste_and_hide(
window: >k::Window,
tree_view: >k::TreeView,
sorted_store: >k::TreeModelSort,
search_entry: >k::SearchEntry,
) -> Result<(), Error> {
if let Some((_, row)) = tree_view.selection().selected() {
let model = tree_view.model().ok_or(Error::NoTreeViewModel)?;
let str: String = model.get_value(&row, 1).get().map_err(|_| Error::ModelStringRead)?;
let display = gdk::Display::default().ok_or(Error::NoDisplay)?;
TextClipboard::new(&display).set(&str);
}
hide(window, search_entry, tree_view, sorted_store);
Ok(())
}
fn hide(
window: >k::Window,
search_entry: >k::SearchEntry,
tree_view: >k::TreeView,
sorted_store: >k::TreeModelSort,
) {
window.hide();
search_entry.set_text("");
if let Some(first_row) = sorted_store.iter_first() {
tree_view.selection().select_iter(&first_row);
}
}
fn move_selection(tree_view: >k::TreeView, up: bool) {
let selection = tree_view.selection();
if let Some((tree_model, mut tree_iter)) = selection.selected() {
let moved = match up {
true => tree_model.iter_previous(&mut tree_iter),
false => tree_model.iter_next(&mut tree_iter),
};
if moved {
selection.select_iter(&tree_iter);
}
}
}
fn setup_tree_columns(tree_view: >k::TreeView) {
{
let renderer = gtk::CellRendererText::new();
let column = gtk::TreeViewColumn::new();
column.pack_start(&renderer, true);
column.add_attribute(&renderer, "text", 0);
column.set_sort_order(gtk::SortType::Descending);
tree_view.append_column(&column);
}
{
let renderer = gtk::CellRendererText::new();
let column = gtk::TreeViewColumn::new();
column.pack_start(&renderer, true);
column.add_attribute(&renderer, "text", 1);
tree_view.append_column(&column);
}
{
let column = gtk::TreeViewColumn::new();
column.set_title("Value");
column.set_clickable(true);
column.set_sort_indicator(true);
column.set_sort_column_id(2);
tree_view.append_column(&column);
}
}
fn create_store(config: &Config) -> gtk::TreeStore {
let store = gtk::TreeStore::new(&[Type::STRING, Type::STRING, Type::I64]);
for alias in config.aliases() {
store.set(&store.append(None), &[(0, &alias.key), (1, &alias.value), (2, &0i64)]);
}
store
}
fn connect_search_handler(
search_entry: >k::SearchEntry,
tree_view: >k::TreeView,
sorted_store: >k::TreeModelSort,
store: gtk::TreeStore,
config: Config,
) {
search_entry.connect_search_changed({
let tree_view = tree_view.clone();
let sorted_store = sorted_store.clone();
move |entry| {
let text = entry.text().to_string();
let mut scored_aliases: Vec<(Alias, i64)> = config
.aliases()
.par_bridge()
.map(|alias| {
let score: i64 = FuzzySearch::new(&text, alias.key)
.best_match()
.map_or(0, |m| m.score() as i64);
(alias, score)
})
.collect();
scored_aliases.sort_by_key(|(alias, score)| (*score, alias.key));
store.clear();
scored_aliases.iter().for_each(|(alias, score)| {
store.set(&store.append(None), &[(0, &alias.key), (1, &alias.value), (2, &score)]);
});
tree_view.selection().unselect_all();
if let Some(first_row) = sorted_store.iter_first() {
tree_view.selection().select_iter(&first_row);
}
}
});
}
fn spawn_show_listener(show_listener: Arc<UnixListener>, window: >k::Window) {
let (sender, receiver) = async_channel::bounded(1);
std::thread::spawn(move || {
for _ in show_listener.incoming() {
if sender.send_blocking(()).is_err() {
break;
}
}
});
glib::spawn_future_local({
let window = window.clone();
async move {
loop {
if receiver.recv().await.is_ok() {
window.show();
}
}
}
});
}
fn create_key_controller(
window: >k::Window,
tree_view: >k::TreeView,
sorted_store: >k::TreeModelSort,
search_entry: >k::SearchEntry,
) -> gtk::EventControllerKey {
let controller = gtk::EventControllerKey::new();
controller.connect_key_released({
let window = window.clone();
let tree_view = tree_view.clone();
let sorted_store = sorted_store.clone();
let search_entry = search_entry.clone();
move |_, key, _, _| match key {
Key::Return => {
if let Err(e) = paste_and_hide(&window, &tree_view, &sorted_store, &search_entry) {
eprintln!("error: {e}");
}
}
Key::Escape => {
hide(&window, &search_entry, &tree_view, &sorted_store);
}
_ => (),
}
});
controller.connect_key_pressed({
let tree_view = tree_view.clone();
move |_, key, _, _| match key {
Key::Up => {
move_selection(&tree_view, true);
Propagation::Stop
}
Key::Down => {
move_selection(&tree_view, false);
Propagation::Stop
}
_ => Propagation::Proceed,
}
});
controller
}
fn set_size_proportional_to_screen(
window: >k::Window,
width_fraction: f64,
height_fraction: f64,
) -> Result<(), Error> {
let display = gdk::Display::default().ok_or(Error::NoDisplay)?;
let monitor = display
.monitors()
.item(0)
.and_then(|m| m.downcast::<gdk::Monitor>().ok())
.ok_or(Error::NoDisplay)?;
let geometry = monitor.geometry();
#[allow(clippy::cast_possible_truncation)]
let target_width = (f64::from(geometry.width()) * width_fraction) as i32;
#[allow(clippy::cast_possible_truncation)]
let target_height = (f64::from(geometry.height()) * height_fraction) as i32;
window.set_default_size(target_width, target_height);
Ok(())
}
fn build_ui(
app: >k::Application,
config: Config,
show_listener: Arc<UnixListener>,
) -> Result<(), Error> {
let builder = gtk::Builder::from_string(include_str!("window_main.ui"));
let window: gtk::Window =
builder.object("window_main").ok_or(Error::MissingWidget("window_main"))?;
window.set_application(Some(app));
let tree_view: gtk::TreeView =
builder.object("tree_view").ok_or(Error::MissingWidget("tree_view"))?;
setup_tree_columns(&tree_view);
let store = create_store(&config);
let sorted_store = gtk::TreeModelSort::with_model(&store);
sorted_store.set_sort_column_id(gtk::SortColumn::Index(2), gtk::SortType::Descending);
tree_view.set_model(Some(&sorted_store));
if let Some(first_row) = sorted_store.iter_first() {
tree_view.selection().select_iter(&first_row);
}
let search_entry: gtk::SearchEntry =
builder.object("search_entry").ok_or(Error::MissingWidget("search_entry"))?;
connect_search_handler(&search_entry, &tree_view, &sorted_store, store, config);
spawn_show_listener(show_listener, &window);
let key_controller = create_key_controller(&window, &tree_view, &sorted_store, &search_entry);
window.add_controller(key_controller);
if let Err(e) = set_size_proportional_to_screen(
&window,
WINDOW_SCREEN_WIDTH_FRACTION,
WINDOW_SCREEN_HEIGHT_FRACTION,
) {
eprintln!("warning: could not set window size: {e}");
}
window.show();
Ok(())
}
fn on_activate(app: >k::Application, show_listener: &Arc<UnixListener>) -> Result<(), Error> {
Config::create_file_if_nonexistent()?;
let config = Config::from_config_file()?;
build_ui(app, config, Arc::clone(show_listener))
}
fn launch_application(show_listener: UnixListener) {
let application = gtk::Application::new(None::<&str>, gio::ApplicationFlags::default());
let show_listener: Arc<UnixListener> = Arc::new(show_listener);
application.connect_activate(move |app| {
if let Err(e) = on_activate(app, &show_listener) {
eprintln!("error: {e}");
app.quit();
}
});
application.run();
}
enum SendShowSignalOrListen {
Listener(UnixListener),
SignalSent,
}
fn send_show_signal_or_listen(
socket_path: impl AsRef<Path>,
) -> Result<SendShowSignalOrListen, Error> {
use std::io::ErrorKind;
match UnixListener::bind(&socket_path) {
Ok(listener) => Ok(SendShowSignalOrListen::Listener(listener)),
Err(e) if e.kind() == ErrorKind::AddrInUse => {
match UnixStream::connect(&socket_path) {
Ok(_) => Ok(SendShowSignalOrListen::SignalSent),
Err(e) if e.kind() == ErrorKind::ConnectionRefused => {
std::fs::remove_file(&socket_path).map_err(Error::Socket)?;
UnixListener::bind(&socket_path)
.map(SendShowSignalOrListen::Listener)
.map_err(Error::Socket)
}
Err(e) => Err(Error::Socket(e)),
}
}
Err(e) => Err(Error::Socket(e)),
}
}
fn unix_socket_show_signal_path() -> Result<PathBuf, Error> {
Ok(dirs::runtime_dir().ok_or(Error::NoRuntimeDir)?.join("shrug_show.sock"))
}
fn run() -> Result<(), Error> {
match send_show_signal_or_listen(unix_socket_show_signal_path()?)? {
SendShowSignalOrListen::Listener(listener) => {
launch_application(listener);
}
SendShowSignalOrListen::SignalSent => {
println!("sent signal to running shrug.");
}
}
Ok(())
}
fn main() {
if let Err(e) = run() {
eprintln!("error: {e}");
}
}