Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 536c5c5012 | |||
| abd4df6939 | |||
| 43f7228be2 | |||
| a1b47b8ba0 | |||
| ccce9b8572 | |||
| ffb4c2f127 | |||
| cde599db03 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,3 +11,5 @@ aur/*/*.tar.gz
|
||||
aur/*/*.tar.xz
|
||||
aur/*/*.pkg.tar.*
|
||||
# Keep PKGBUILD and .SRCINFO tracked
|
||||
.SRCINFO
|
||||
aur/
|
||||
|
||||
709
Cargo.lock
generated
709
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-lua"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-api"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-bookmarks"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
@@ -25,3 +25,7 @@ dirs = "5.0"
|
||||
# For parsing Chrome bookmarks JSON
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
||||
# For reading Firefox bookmarks (places.sqlite)
|
||||
sqlx = { version = "0.8", features = ["runtime-tokio", "sqlite"] }
|
||||
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
//! Bookmarks Plugin for Owlry
|
||||
//!
|
||||
//! A static provider that reads browser bookmarks from Chrome/Chromium.
|
||||
//! Firefox support would require the rusqlite crate for reading places.sqlite.
|
||||
//! A static provider that reads browser bookmarks from various browsers.
|
||||
//!
|
||||
//! Supported browsers:
|
||||
//! - Firefox (via places.sqlite using SQLx)
|
||||
//! - Chrome
|
||||
//! - Chromium
|
||||
//! - Brave
|
||||
@@ -14,8 +14,14 @@ use owlry_plugin_api::{
|
||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use sqlx::Row;
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
use std::io::Write;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
// Plugin metadata
|
||||
const PLUGIN_ID: &str = "bookmarks";
|
||||
@@ -27,17 +33,141 @@ const PLUGIN_DESCRIPTION: &str = "Browser bookmark search";
|
||||
const PROVIDER_ID: &str = "bookmarks";
|
||||
const PROVIDER_NAME: &str = "Bookmarks";
|
||||
const PROVIDER_PREFIX: &str = ":bm";
|
||||
const PROVIDER_ICON: &str = "web-browser";
|
||||
const PROVIDER_ICON: &str = "user-bookmarks-symbolic";
|
||||
const PROVIDER_TYPE_ID: &str = "bookmarks";
|
||||
|
||||
/// Bookmarks provider state - holds cached items
|
||||
struct BookmarksState {
|
||||
/// Cached bookmark items (returned immediately on refresh)
|
||||
items: Vec<PluginItem>,
|
||||
/// Flag to prevent concurrent background loads
|
||||
loading: Arc<AtomicBool>,
|
||||
}
|
||||
|
||||
impl BookmarksState {
|
||||
fn new() -> Self {
|
||||
Self { items: Vec::new() }
|
||||
Self {
|
||||
items: Vec::new(),
|
||||
loading: Arc::new(AtomicBool::new(false)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get or create the favicon cache directory
|
||||
fn favicon_cache_dir() -> Option<PathBuf> {
|
||||
dirs::cache_dir().map(|d| d.join("owlry/favicons"))
|
||||
}
|
||||
|
||||
/// Ensure the favicon cache directory exists
|
||||
fn ensure_favicon_cache_dir() -> Option<PathBuf> {
|
||||
Self::favicon_cache_dir().and_then(|dir| {
|
||||
fs::create_dir_all(&dir).ok()?;
|
||||
Some(dir)
|
||||
})
|
||||
}
|
||||
|
||||
/// Hash a URL to create a cache filename
|
||||
fn url_to_cache_filename(url: &str) -> String {
|
||||
use std::hash::{Hash, Hasher};
|
||||
let mut hasher = std::collections::hash_map::DefaultHasher::new();
|
||||
url.hash(&mut hasher);
|
||||
format!("{:016x}.png", hasher.finish())
|
||||
}
|
||||
|
||||
/// Get the bookmark cache file path
|
||||
fn bookmark_cache_file() -> Option<PathBuf> {
|
||||
dirs::cache_dir().map(|d| d.join("owlry/bookmarks.json"))
|
||||
}
|
||||
|
||||
/// Load cached bookmarks from disk (fast)
|
||||
fn load_cached_bookmarks() -> Vec<PluginItem> {
|
||||
let cache_file = match Self::bookmark_cache_file() {
|
||||
Some(f) => f,
|
||||
None => return Vec::new(),
|
||||
};
|
||||
|
||||
if !cache_file.exists() {
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let content = match fs::read_to_string(&cache_file) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
// Parse cached bookmarks (simple JSON format)
|
||||
#[derive(serde::Deserialize)]
|
||||
struct CachedBookmark {
|
||||
id: String,
|
||||
name: String,
|
||||
command: String,
|
||||
description: Option<String>,
|
||||
icon: String,
|
||||
}
|
||||
|
||||
let cached: Vec<CachedBookmark> = match serde_json::from_str(&content) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
cached
|
||||
.into_iter()
|
||||
.map(|b| {
|
||||
let mut item = PluginItem::new(b.id, b.name, b.command)
|
||||
.with_icon(&b.icon)
|
||||
.with_keywords(vec!["bookmark".to_string()]);
|
||||
if let Some(desc) = b.description {
|
||||
item = item.with_description(desc);
|
||||
}
|
||||
item
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Save bookmarks to cache file
|
||||
fn save_cached_bookmarks(items: &[PluginItem]) {
|
||||
let cache_file = match Self::bookmark_cache_file() {
|
||||
Some(f) => f,
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Ensure cache directory exists
|
||||
if let Some(parent) = cache_file.parent() {
|
||||
let _ = fs::create_dir_all(parent);
|
||||
}
|
||||
|
||||
#[derive(serde::Serialize)]
|
||||
struct CachedBookmark {
|
||||
id: String,
|
||||
name: String,
|
||||
command: String,
|
||||
description: Option<String>,
|
||||
icon: String,
|
||||
}
|
||||
|
||||
let cached: Vec<CachedBookmark> = items
|
||||
.iter()
|
||||
.map(|item| {
|
||||
let desc: Option<String> = match &item.description {
|
||||
abi_stable::std_types::ROption::RSome(s) => Some(s.to_string()),
|
||||
abi_stable::std_types::ROption::RNone => None,
|
||||
};
|
||||
let icon: String = match &item.icon {
|
||||
abi_stable::std_types::ROption::RSome(s) => s.to_string(),
|
||||
abi_stable::std_types::ROption::RNone => PROVIDER_ICON.to_string(),
|
||||
};
|
||||
CachedBookmark {
|
||||
id: item.id.to_string(),
|
||||
name: item.name.to_string(),
|
||||
command: item.command.to_string(),
|
||||
description: desc,
|
||||
icon,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Ok(json) = serde_json::to_string(&cached) {
|
||||
let _ = fs::write(&cache_file, json);
|
||||
}
|
||||
}
|
||||
|
||||
fn chromium_bookmark_paths() -> Vec<PathBuf> {
|
||||
@@ -61,18 +191,87 @@ impl BookmarksState {
|
||||
paths
|
||||
}
|
||||
|
||||
fn load_bookmarks(&mut self) {
|
||||
self.items.clear();
|
||||
fn firefox_places_paths() -> Vec<PathBuf> {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
// Load Chrome/Chromium bookmarks
|
||||
for path in Self::chromium_bookmark_paths() {
|
||||
if path.exists() {
|
||||
self.read_chrome_bookmarks(&path);
|
||||
if let Some(home) = dirs::home_dir() {
|
||||
let firefox_dir = home.join(".mozilla/firefox");
|
||||
if firefox_dir.exists() {
|
||||
// Find all profile directories
|
||||
if let Ok(entries) = fs::read_dir(&firefox_dir) {
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
let places = path.join("places.sqlite");
|
||||
if places.exists() {
|
||||
paths.push(places);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
paths
|
||||
}
|
||||
|
||||
/// Find Firefox favicons.sqlite paths (paired with places.sqlite)
|
||||
fn firefox_favicons_path(places_path: &Path) -> Option<PathBuf> {
|
||||
let favicons = places_path.parent()?.join("favicons.sqlite");
|
||||
if favicons.exists() {
|
||||
Some(favicons)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn read_chrome_bookmarks(&mut self, path: &PathBuf) {
|
||||
fn load_bookmarks(&mut self) {
|
||||
// Fast path: load from cache immediately
|
||||
if self.items.is_empty() {
|
||||
self.items = Self::load_cached_bookmarks();
|
||||
}
|
||||
|
||||
// Don't start another background load if one is already running
|
||||
if self.loading.swap(true, Ordering::SeqCst) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Spawn background thread to refresh bookmarks
|
||||
let loading = self.loading.clone();
|
||||
thread::spawn(move || {
|
||||
let mut items = Vec::new();
|
||||
|
||||
// Load Chrome/Chromium bookmarks (fast - just JSON parsing)
|
||||
for path in Self::chromium_bookmark_paths() {
|
||||
if path.exists() {
|
||||
Self::read_chrome_bookmarks_static(&path, &mut items);
|
||||
}
|
||||
}
|
||||
|
||||
// Load Firefox bookmarks with favicons (async via tokio)
|
||||
let rt = match tokio::runtime::Runtime::new() {
|
||||
Ok(rt) => rt,
|
||||
Err(_) => {
|
||||
loading.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
for path in Self::firefox_places_paths() {
|
||||
rt.block_on(async {
|
||||
Self::read_firefox_bookmarks_async(&path, &mut items).await;
|
||||
});
|
||||
}
|
||||
|
||||
// Save to cache for next startup
|
||||
Self::save_cached_bookmarks(&items);
|
||||
|
||||
loading.store(false, Ordering::SeqCst);
|
||||
});
|
||||
}
|
||||
|
||||
/// Read Chrome bookmarks (static helper for background thread)
|
||||
fn read_chrome_bookmarks_static(path: &PathBuf, items: &mut Vec<PluginItem>) {
|
||||
let content = match fs::read_to_string(path) {
|
||||
Ok(c) => c,
|
||||
Err(_) => return,
|
||||
@@ -83,29 +282,27 @@ impl BookmarksState {
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
// Process bookmark bar and other folders
|
||||
if let Some(roots) = bookmarks.roots {
|
||||
if let Some(bar) = roots.bookmark_bar {
|
||||
self.process_chrome_folder(&bar);
|
||||
Self::process_chrome_folder_static(&bar, items);
|
||||
}
|
||||
if let Some(other) = roots.other {
|
||||
self.process_chrome_folder(&other);
|
||||
Self::process_chrome_folder_static(&other, items);
|
||||
}
|
||||
if let Some(synced) = roots.synced {
|
||||
self.process_chrome_folder(&synced);
|
||||
Self::process_chrome_folder_static(&synced, items);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn process_chrome_folder(&mut self, folder: &ChromeBookmarkNode) {
|
||||
fn process_chrome_folder_static(folder: &ChromeBookmarkNode, items: &mut Vec<PluginItem>) {
|
||||
if let Some(ref children) = folder.children {
|
||||
for child in children {
|
||||
match child.node_type.as_deref() {
|
||||
Some("url") => {
|
||||
if let Some(ref url) = child.url {
|
||||
let name = child.name.clone().unwrap_or_else(|| url.clone());
|
||||
|
||||
self.items.push(
|
||||
items.push(
|
||||
PluginItem::new(
|
||||
format!("bookmark:{}", url),
|
||||
name,
|
||||
@@ -113,19 +310,199 @@ impl BookmarksState {
|
||||
)
|
||||
.with_description(url.clone())
|
||||
.with_icon(PROVIDER_ICON)
|
||||
.with_keywords(vec!["bookmark".to_string(), "web".to_string()]),
|
||||
.with_keywords(vec!["bookmark".to_string(), "chrome".to_string()]),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some("folder") => {
|
||||
// Recursively process subfolders
|
||||
self.process_chrome_folder(child);
|
||||
Self::process_chrome_folder_static(child, items);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Read Firefox bookmarks asynchronously
|
||||
async fn read_firefox_bookmarks_async(places_path: &PathBuf, items: &mut Vec<PluginItem>) {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let temp_db = temp_dir.join("owlry_places_temp.sqlite");
|
||||
|
||||
if fs::copy(places_path, &temp_db).is_err() {
|
||||
return;
|
||||
}
|
||||
|
||||
let wal_path = places_path.with_extension("sqlite-wal");
|
||||
if wal_path.exists() {
|
||||
let temp_wal = temp_db.with_extension("sqlite-wal");
|
||||
let _ = fs::copy(&wal_path, &temp_wal);
|
||||
}
|
||||
|
||||
let favicons_path = Self::firefox_favicons_path(places_path);
|
||||
let temp_favicons = temp_dir.join("owlry_favicons_temp.sqlite");
|
||||
if let Some(ref fp) = favicons_path {
|
||||
let _ = fs::copy(fp, &temp_favicons);
|
||||
let fav_wal = fp.with_extension("sqlite-wal");
|
||||
if fav_wal.exists() {
|
||||
let _ = fs::copy(&fav_wal, temp_favicons.with_extension("sqlite-wal"));
|
||||
}
|
||||
}
|
||||
|
||||
let db_url = format!("sqlite:{}?mode=ro", temp_db.display());
|
||||
let favicons_url = if favicons_path.is_some() {
|
||||
Some(format!("sqlite:{}?mode=ro", temp_favicons.display()))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let cache_dir = Self::ensure_favicon_cache_dir();
|
||||
|
||||
let bookmarks = Self::fetch_firefox_bookmarks_with_favicons(
|
||||
&db_url,
|
||||
favicons_url.as_deref(),
|
||||
cache_dir.as_ref(),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Clean up temp files
|
||||
let _ = fs::remove_file(&temp_db);
|
||||
let _ = fs::remove_file(temp_db.with_extension("sqlite-wal"));
|
||||
let _ = fs::remove_file(&temp_favicons);
|
||||
let _ = fs::remove_file(temp_favicons.with_extension("sqlite-wal"));
|
||||
|
||||
for (title, url, favicon_path) in bookmarks {
|
||||
let icon = favicon_path.unwrap_or_else(|| PROVIDER_ICON.to_string());
|
||||
items.push(
|
||||
PluginItem::new(
|
||||
format!("bookmark:firefox:{}", url),
|
||||
title,
|
||||
format!("xdg-open '{}'", url.replace('\'', "'\\''")),
|
||||
)
|
||||
.with_description(url)
|
||||
.with_icon(&icon)
|
||||
.with_keywords(vec!["bookmark".to_string(), "firefox".to_string()]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fetch Firefox bookmarks with their favicons
|
||||
async fn fetch_firefox_bookmarks_with_favicons(
|
||||
places_url: &str,
|
||||
favicons_url: Option<&str>,
|
||||
cache_dir: Option<&PathBuf>,
|
||||
) -> Vec<(String, String, Option<String>)> {
|
||||
// First, fetch bookmarks from places.sqlite
|
||||
let pool = match SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(places_url)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
// Query bookmarks joining moz_bookmarks with moz_places
|
||||
// type=1 means URL bookmarks (not folders, separators, etc.)
|
||||
let query = r#"
|
||||
SELECT b.title, p.url
|
||||
FROM moz_bookmarks b
|
||||
JOIN moz_places p ON b.fk = p.id
|
||||
WHERE b.type = 1
|
||||
AND p.url NOT LIKE 'place:%'
|
||||
AND p.url NOT LIKE 'about:%'
|
||||
AND b.title IS NOT NULL
|
||||
AND b.title != ''
|
||||
ORDER BY b.dateAdded DESC
|
||||
LIMIT 500
|
||||
"#;
|
||||
|
||||
let rows = match sqlx::query(query).fetch_all(&pool).await {
|
||||
Ok(r) => r,
|
||||
Err(_) => return Vec::new(),
|
||||
};
|
||||
|
||||
let bookmarks: Vec<(String, String)> = rows
|
||||
.into_iter()
|
||||
.filter_map(|row| {
|
||||
let title: Option<String> = row.get("title");
|
||||
let url: Option<String> = row.get("url");
|
||||
match (title, url) {
|
||||
(Some(t), Some(u)) => Some((t, u)),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
// If no favicons database or cache dir, return without favicons
|
||||
let (favicons_url, cache_dir) = match (favicons_url, cache_dir) {
|
||||
(Some(f), Some(c)) => (f, c),
|
||||
_ => return bookmarks.into_iter().map(|(t, u)| (t, u, None)).collect(),
|
||||
};
|
||||
|
||||
// Connect to favicons database
|
||||
let fav_pool = match SqlitePoolOptions::new()
|
||||
.max_connections(1)
|
||||
.connect(favicons_url)
|
||||
.await
|
||||
{
|
||||
Ok(p) => p,
|
||||
Err(_) => return bookmarks.into_iter().map(|(t, u)| (t, u, None)).collect(),
|
||||
};
|
||||
|
||||
// Fetch favicons for each URL
|
||||
let mut results = Vec::new();
|
||||
for (title, url) in bookmarks {
|
||||
let favicon_path = Self::get_favicon_for_url(&fav_pool, &url, cache_dir).await;
|
||||
results.push((title, url, favicon_path));
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
/// Get favicon for a URL, caching to file if needed
|
||||
async fn get_favicon_for_url(
|
||||
pool: &sqlx::SqlitePool,
|
||||
page_url: &str,
|
||||
cache_dir: &Path,
|
||||
) -> Option<String> {
|
||||
// Check if already cached
|
||||
let cache_filename = Self::url_to_cache_filename(page_url);
|
||||
let cache_path = cache_dir.join(&cache_filename);
|
||||
if cache_path.exists() {
|
||||
return Some(cache_path.to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
// Query favicon data from database
|
||||
// Join moz_pages_w_icons -> moz_icons_to_pages -> moz_icons
|
||||
// Prefer smaller icons (32px) for efficiency
|
||||
let query = r#"
|
||||
SELECT i.data
|
||||
FROM moz_pages_w_icons p
|
||||
JOIN moz_icons_to_pages ip ON p.id = ip.page_id
|
||||
JOIN moz_icons i ON ip.icon_id = i.id
|
||||
WHERE p.page_url = ?
|
||||
AND i.data IS NOT NULL
|
||||
ORDER BY ABS(i.width - 32) ASC
|
||||
LIMIT 1
|
||||
"#;
|
||||
|
||||
let row = sqlx::query(query)
|
||||
.bind(page_url)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.ok()??;
|
||||
|
||||
let data: Vec<u8> = row.get("data");
|
||||
if data.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Write favicon data to cache file
|
||||
let mut file = fs::File::create(&cache_path).ok()?;
|
||||
file.write_all(&data).ok()?;
|
||||
|
||||
Some(cache_path.to_string_lossy().to_string())
|
||||
}
|
||||
}
|
||||
|
||||
// Chrome bookmark JSON structures
|
||||
@@ -241,6 +618,14 @@ mod tests {
|
||||
assert!(!paths.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_firefox_paths() {
|
||||
// This will find paths if Firefox is installed
|
||||
let paths = BookmarksState::firefox_places_paths();
|
||||
// Path detection should work (may be empty if Firefox not installed)
|
||||
assert!(paths.len() >= 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_chrome_bookmarks() {
|
||||
let json = r#"{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-calculator"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-clipboard"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-emoji"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -423,6 +423,7 @@ impl EmojiState {
|
||||
name.to_string(),
|
||||
format!("printf '%s' '{}' | wl-copy", emoji),
|
||||
)
|
||||
.with_icon(*emoji) // Use emoji character as icon
|
||||
.with_description(format!("{} {}", emoji, keywords))
|
||||
.with_keywords(vec![name.to_string(), keywords.to_string()]),
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-filesearch"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-media"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-pomodoro"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -57,31 +57,31 @@ impl PomodoroConfig {
|
||||
let config_content = config_path
|
||||
.and_then(|p| fs::read_to_string(p).ok());
|
||||
|
||||
if let Some(content) = config_content {
|
||||
if let Ok(toml) = content.parse::<toml::Table>() {
|
||||
// Try [plugins.pomodoro] first (new format)
|
||||
if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table()) {
|
||||
if let Some(pomodoro) = plugins.get("pomodoro").and_then(|v| v.as_table()) {
|
||||
return Self::from_toml_table(pomodoro);
|
||||
}
|
||||
}
|
||||
if let Some(content) = config_content
|
||||
&& let Ok(toml) = content.parse::<toml::Table>()
|
||||
{
|
||||
// Try [plugins.pomodoro] first (new format)
|
||||
if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table())
|
||||
&& let Some(pomodoro) = plugins.get("pomodoro").and_then(|v| v.as_table())
|
||||
{
|
||||
return Self::from_toml_table(pomodoro);
|
||||
}
|
||||
|
||||
// Fallback to [providers] section (old format)
|
||||
if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) {
|
||||
let work_mins = providers
|
||||
.get("pomodoro_work_mins")
|
||||
.and_then(|v| v.as_integer())
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(DEFAULT_WORK_MINS);
|
||||
// Fallback to [providers] section (old format)
|
||||
if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) {
|
||||
let work_mins = providers
|
||||
.get("pomodoro_work_mins")
|
||||
.and_then(|v| v.as_integer())
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(DEFAULT_WORK_MINS);
|
||||
|
||||
let break_mins = providers
|
||||
.get("pomodoro_break_mins")
|
||||
.and_then(|v| v.as_integer())
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(DEFAULT_BREAK_MINS);
|
||||
let break_mins = providers
|
||||
.get("pomodoro_break_mins")
|
||||
.and_then(|v| v.as_integer())
|
||||
.map(|v| v as u32)
|
||||
.unwrap_or(DEFAULT_BREAK_MINS);
|
||||
|
||||
return Self { work_mins, break_mins };
|
||||
}
|
||||
return Self { work_mins, break_mins };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-scripts"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-ssh"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-system"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-systemd"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-weather"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -82,41 +82,41 @@ impl WeatherConfig {
|
||||
let config_content = config_path
|
||||
.and_then(|p| fs::read_to_string(p).ok());
|
||||
|
||||
if let Some(content) = config_content {
|
||||
if let Ok(toml) = content.parse::<toml::Table>() {
|
||||
// Try [plugins.weather] first (new format)
|
||||
if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table()) {
|
||||
if let Some(weather) = plugins.get("weather").and_then(|v| v.as_table()) {
|
||||
return Self::from_toml_table(weather);
|
||||
}
|
||||
}
|
||||
if let Some(content) = config_content
|
||||
&& let Ok(toml) = content.parse::<toml::Table>()
|
||||
{
|
||||
// Try [plugins.weather] first (new format)
|
||||
if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table())
|
||||
&& let Some(weather) = plugins.get("weather").and_then(|v| v.as_table())
|
||||
{
|
||||
return Self::from_toml_table(weather);
|
||||
}
|
||||
|
||||
// Fallback to [providers] section (old format)
|
||||
if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) {
|
||||
let provider_str = providers
|
||||
.get("weather_provider")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("wttr.in");
|
||||
// Fallback to [providers] section (old format)
|
||||
if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) {
|
||||
let provider_str = providers
|
||||
.get("weather_provider")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("wttr.in");
|
||||
|
||||
let provider = provider_str.parse().unwrap_or(WeatherProviderType::WttrIn);
|
||||
let provider = provider_str.parse().unwrap_or(WeatherProviderType::WttrIn);
|
||||
|
||||
let api_key = providers
|
||||
.get("weather_api_key")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
let api_key = providers
|
||||
.get("weather_api_key")
|
||||
.and_then(|v| v.as_str())
|
||||
.map(String::from);
|
||||
|
||||
let location = providers
|
||||
.get("weather_location")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
let location = providers
|
||||
.get("weather_location")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or("")
|
||||
.to_string();
|
||||
|
||||
return Self {
|
||||
provider,
|
||||
api_key,
|
||||
location,
|
||||
};
|
||||
}
|
||||
return Self {
|
||||
provider,
|
||||
api_key,
|
||||
location,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-plugin-websearch"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry-rune"
|
||||
version = "0.2.0"
|
||||
version = "0.2.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
description = "Rune scripting runtime for owlry plugins"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "owlry"
|
||||
version = "0.4.0"
|
||||
version = "0.4.1"
|
||||
edition = "2024"
|
||||
rust-version = "1.90"
|
||||
description = "A lightweight, owl-themed application launcher for Wayland"
|
||||
|
||||
@@ -56,7 +56,10 @@ impl OwlryApp {
|
||||
let native_providers = Self::load_native_plugins(&config.borrow());
|
||||
|
||||
// Create provider manager with native plugins
|
||||
#[cfg(feature = "lua")]
|
||||
let mut provider_manager = ProviderManager::with_native_plugins(native_providers);
|
||||
#[cfg(not(feature = "lua"))]
|
||||
let provider_manager = ProviderManager::with_native_plugins(native_providers);
|
||||
|
||||
// Load Lua plugins if enabled (requires lua feature)
|
||||
#[cfg(feature = "lua")]
|
||||
@@ -75,7 +78,7 @@ impl OwlryApp {
|
||||
);
|
||||
let filter = Rc::new(RefCell::new(filter));
|
||||
|
||||
let window = MainWindow::new(app, config.clone(), providers.clone(), frecency.clone(), filter.clone());
|
||||
let window = MainWindow::new(app, config.clone(), providers.clone(), frecency.clone(), filter.clone(), args.prompt.clone());
|
||||
|
||||
// Set up layer shell for Wayland overlay behavior
|
||||
window.init_layer_shell();
|
||||
|
||||
@@ -21,6 +21,10 @@ pub struct CliArgs {
|
||||
#[arg(long, short = 'p', value_delimiter = ',', value_parser = parse_provider)]
|
||||
pub providers: Option<Vec<ProviderType>>,
|
||||
|
||||
/// Custom prompt text for the search input (useful for dmenu mode)
|
||||
#[arg(long)]
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Subcommand to run (if any)
|
||||
#[command(subcommand)]
|
||||
pub command: Option<Command>,
|
||||
|
||||
@@ -6,10 +6,13 @@ use std::process::Command;
|
||||
|
||||
use crate::paths;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct Config {
|
||||
#[serde(default)]
|
||||
pub general: GeneralConfig,
|
||||
#[serde(default)]
|
||||
pub appearance: AppearanceConfig,
|
||||
#[serde(default)]
|
||||
pub providers: ProvidersConfig,
|
||||
#[serde(default)]
|
||||
pub plugins: PluginsConfig,
|
||||
@@ -17,9 +20,13 @@ pub struct Config {
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct GeneralConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub show_icons: bool,
|
||||
#[serde(default = "default_max_results")]
|
||||
pub max_results: usize,
|
||||
pub terminal_command: String,
|
||||
/// Terminal command (auto-detected if not specified)
|
||||
#[serde(default)]
|
||||
pub terminal_command: Option<String>,
|
||||
/// Launch wrapper command for app execution.
|
||||
/// Examples: "uwsm app --", "hyprctl dispatch exec --", "systemd-run --user --"
|
||||
/// If None or empty, launches directly via sh -c
|
||||
@@ -31,6 +38,22 @@ pub struct GeneralConfig {
|
||||
pub tabs: Vec<String>,
|
||||
}
|
||||
|
||||
impl Default for GeneralConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
show_icons: true,
|
||||
max_results: 100,
|
||||
terminal_command: None,
|
||||
launch_wrapper: None,
|
||||
tabs: default_tabs(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_max_results() -> usize {
|
||||
100
|
||||
}
|
||||
|
||||
fn default_tabs() -> Vec<String> {
|
||||
vec![
|
||||
"app".to_string(),
|
||||
@@ -40,9 +63,10 @@ fn default_tabs() -> Vec<String> {
|
||||
}
|
||||
|
||||
/// User-customizable theme colors
|
||||
/// All fields are optional - unset values inherit from GTK theme
|
||||
/// All fields are optional - unset values inherit from theme or GTK defaults
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
pub struct ThemeColors {
|
||||
// Core colors
|
||||
pub background: Option<String>,
|
||||
pub background_secondary: Option<String>,
|
||||
pub border: Option<String>,
|
||||
@@ -64,13 +88,21 @@ pub struct ThemeColors {
|
||||
pub badge_sys: Option<String>,
|
||||
pub badge_uuctl: Option<String>,
|
||||
pub badge_web: Option<String>,
|
||||
// Widget badge colors
|
||||
pub badge_media: Option<String>,
|
||||
pub badge_weather: Option<String>,
|
||||
pub badge_pomo: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AppearanceConfig {
|
||||
#[serde(default = "default_width")]
|
||||
pub width: i32,
|
||||
#[serde(default = "default_height")]
|
||||
pub height: i32,
|
||||
#[serde(default = "default_font_size")]
|
||||
pub font_size: u32,
|
||||
#[serde(default = "default_border_radius")]
|
||||
pub border_radius: u32,
|
||||
/// Theme name: None = GTK default, "owl" = built-in owl theme
|
||||
#[serde(default)]
|
||||
@@ -80,10 +112,31 @@ pub struct AppearanceConfig {
|
||||
pub colors: ThemeColors,
|
||||
}
|
||||
|
||||
impl Default for AppearanceConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
width: 850,
|
||||
height: 650,
|
||||
font_size: 14,
|
||||
border_radius: 12,
|
||||
theme: None,
|
||||
colors: ThemeColors::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn default_width() -> i32 { 850 }
|
||||
fn default_height() -> i32 { 650 }
|
||||
fn default_font_size() -> u32 { 14 }
|
||||
fn default_border_radius() -> u32 { 12 }
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ProvidersConfig {
|
||||
#[serde(default = "default_true")]
|
||||
pub applications: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub commands: bool,
|
||||
#[serde(default = "default_true")]
|
||||
pub uuctl: bool,
|
||||
/// Enable calculator provider (= expression or calc expression)
|
||||
#[serde(default = "default_true")]
|
||||
@@ -159,6 +212,36 @@ pub struct ProvidersConfig {
|
||||
pub pomodoro_break_mins: u32,
|
||||
}
|
||||
|
||||
impl Default for ProvidersConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
applications: true,
|
||||
commands: true,
|
||||
uuctl: true,
|
||||
calculator: true,
|
||||
frecency: true,
|
||||
frecency_weight: 0.3,
|
||||
websearch: true,
|
||||
search_engine: "duckduckgo".to_string(),
|
||||
system: true,
|
||||
ssh: true,
|
||||
clipboard: true,
|
||||
bookmarks: true,
|
||||
emoji: true,
|
||||
scripts: true,
|
||||
files: true,
|
||||
media: true,
|
||||
weather: false,
|
||||
weather_provider: "wttr.in".to_string(),
|
||||
weather_api_key: None,
|
||||
weather_location: Some("Berlin".to_string()),
|
||||
pomodoro: false,
|
||||
pomodoro_work_mins: 25,
|
||||
pomodoro_break_mins: 5,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Configuration for plugins
|
||||
///
|
||||
/// Supports per-plugin configuration via `[plugins.<name>]` sections:
|
||||
@@ -450,57 +533,7 @@ fn command_exists(cmd: &str) -> bool {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
let terminal = detect_terminal();
|
||||
info!("Detected terminal: {}", terminal);
|
||||
|
||||
Self {
|
||||
general: GeneralConfig {
|
||||
show_icons: true,
|
||||
max_results: 10,
|
||||
terminal_command: terminal,
|
||||
launch_wrapper: detect_launch_wrapper(),
|
||||
tabs: default_tabs(),
|
||||
},
|
||||
appearance: AppearanceConfig {
|
||||
width: 850,
|
||||
height: 650,
|
||||
font_size: 14,
|
||||
border_radius: 12,
|
||||
theme: None,
|
||||
colors: ThemeColors::default(),
|
||||
},
|
||||
providers: ProvidersConfig {
|
||||
applications: true,
|
||||
commands: true,
|
||||
uuctl: true,
|
||||
calculator: true,
|
||||
frecency: true,
|
||||
frecency_weight: 0.3,
|
||||
websearch: true,
|
||||
search_engine: "duckduckgo".to_string(),
|
||||
system: true,
|
||||
ssh: true,
|
||||
clipboard: true,
|
||||
bookmarks: true,
|
||||
emoji: true,
|
||||
scripts: true,
|
||||
files: true,
|
||||
// Widget providers
|
||||
media: true,
|
||||
weather: false,
|
||||
weather_provider: "wttr.in".to_string(),
|
||||
weather_api_key: None,
|
||||
weather_location: Some("Berlin".to_string()),
|
||||
pomodoro: false,
|
||||
pomodoro_work_mins: 25,
|
||||
pomodoro_break_mins: 5,
|
||||
},
|
||||
plugins: PluginsConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Note: Config derives Default via #[derive(Default)] - all sub-structs have impl Default
|
||||
|
||||
impl Config {
|
||||
pub fn config_path() -> Option<PathBuf> {
|
||||
@@ -517,23 +550,37 @@ impl Config {
|
||||
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
|
||||
let path = Self::config_path().ok_or("Could not determine config path")?;
|
||||
|
||||
if !path.exists() {
|
||||
let mut config = if !path.exists() {
|
||||
info!("Config file not found, using defaults");
|
||||
return Ok(Self::default());
|
||||
Self::default()
|
||||
} else {
|
||||
let content = std::fs::read_to_string(&path)?;
|
||||
let config: Config = toml::from_str(&content)?;
|
||||
info!("Loaded config from {:?}", path);
|
||||
config
|
||||
};
|
||||
|
||||
// Auto-detect terminal if not configured or configured terminal doesn't exist
|
||||
match &config.general.terminal_command {
|
||||
None => {
|
||||
let terminal = detect_terminal();
|
||||
info!("Detected terminal: {}", terminal);
|
||||
config.general.terminal_command = Some(terminal);
|
||||
}
|
||||
Some(term) if !command_exists(term) => {
|
||||
warn!("Configured terminal '{}' not found, auto-detecting", term);
|
||||
let terminal = detect_terminal();
|
||||
info!("Using detected terminal: {}", terminal);
|
||||
config.general.terminal_command = Some(terminal);
|
||||
}
|
||||
Some(term) => {
|
||||
debug!("Using configured terminal: {}", term);
|
||||
}
|
||||
}
|
||||
|
||||
let content = std::fs::read_to_string(&path)?;
|
||||
let mut config: Config = toml::from_str(&content)?;
|
||||
info!("Loaded config from {:?}", path);
|
||||
|
||||
// Validate terminal - if configured terminal doesn't exist, auto-detect
|
||||
if !command_exists(&config.general.terminal_command) {
|
||||
warn!(
|
||||
"Configured terminal '{}' not found, auto-detecting",
|
||||
config.general.terminal_command
|
||||
);
|
||||
config.general.terminal_command = detect_terminal();
|
||||
info!("Using detected terminal: {}", config.general.terminal_command);
|
||||
// Auto-detect launch wrapper if not configured
|
||||
if config.general.launch_wrapper.is_none() {
|
||||
config.general.launch_wrapper = detect_launch_wrapper();
|
||||
}
|
||||
|
||||
Ok(config)
|
||||
|
||||
@@ -64,8 +64,17 @@ impl ProviderFilter {
|
||||
if config_providers.scripts {
|
||||
set.insert(ProviderType::Scripts);
|
||||
}
|
||||
// Note: Files, Calculator, WebSearch are dynamic providers
|
||||
// that don't need to be in the filter set - they're triggered by prefix
|
||||
// Dynamic providers: add to filter set so they work in "All" mode
|
||||
// but can still be excluded when in single-provider mode
|
||||
if config_providers.files {
|
||||
set.insert(ProviderType::Files);
|
||||
}
|
||||
if config_providers.calculator {
|
||||
set.insert(ProviderType::Calculator);
|
||||
}
|
||||
if config_providers.websearch {
|
||||
set.insert(ProviderType::WebSearch);
|
||||
}
|
||||
// Default to apps if nothing enabled
|
||||
if set.is_empty() {
|
||||
set.insert(ProviderType::Application);
|
||||
@@ -104,9 +113,11 @@ impl ProviderFilter {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] Toggled OFF {:?}, enabled: {:?}", provider, self.enabled);
|
||||
} else {
|
||||
#[cfg(feature = "dev-logging")]
|
||||
let provider_debug = format!("{:?}", provider);
|
||||
self.enabled.insert(provider);
|
||||
#[cfg(feature = "dev-logging")]
|
||||
debug!("[Filter] Toggled ON {:?}, enabled: {:?}", provider, self.enabled);
|
||||
debug!("[Filter] Toggled ON {}, enabled: {:?}", provider_debug, self.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -156,6 +156,7 @@ pub fn discover_plugins(plugins_dir: &Path) -> PluginResult<HashMap<String, (Plu
|
||||
}
|
||||
|
||||
/// Check if a plugin is compatible with the given owlry version
|
||||
#[allow(dead_code)]
|
||||
pub fn check_compatibility(manifest: &PluginManifest, owlry_version: &str) -> PluginResult<()> {
|
||||
if !manifest.is_compatible_with(owlry_version) {
|
||||
return Err(PluginError::VersionMismatch {
|
||||
@@ -230,6 +231,7 @@ impl PluginManifest {
|
||||
}
|
||||
|
||||
/// Check if this plugin is compatible with the given owlry version
|
||||
#[allow(dead_code)]
|
||||
pub fn is_compatible_with(&self, owlry_version: &str) -> bool {
|
||||
let req = match semver::VersionReq::parse(&self.plugin.owlry_version) {
|
||||
Ok(r) => r,
|
||||
|
||||
@@ -43,6 +43,7 @@ pub use api::provider::{PluginItem, ProviderRegistration};
|
||||
#[allow(unused_imports)]
|
||||
pub use api::{ActionRegistration, HookEvent, ThemeRegistration};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
pub use error::{PluginError, PluginResult};
|
||||
|
||||
#[cfg(feature = "lua")]
|
||||
|
||||
@@ -80,13 +80,13 @@ impl std::str::FromStr for ProviderType {
|
||||
"cmd" | "command" | "commands" => Ok(ProviderType::Command),
|
||||
"dmenu" => Ok(ProviderType::Dmenu),
|
||||
"emoji" | "emojis" => Ok(ProviderType::Emoji),
|
||||
"file" | "files" | "find" => Ok(ProviderType::Files),
|
||||
"file" | "files" | "find" | "filesearch" => Ok(ProviderType::Files),
|
||||
"media" | "mpris" | "player" => Ok(ProviderType::MediaPlayer),
|
||||
"pomo" | "pomodoro" | "timer" => Ok(ProviderType::Pomodoro),
|
||||
"script" | "scripts" => Ok(ProviderType::Scripts),
|
||||
"ssh" => Ok(ProviderType::Ssh),
|
||||
"sys" | "system" | "power" => Ok(ProviderType::System),
|
||||
"uuctl" => Ok(ProviderType::Uuctl),
|
||||
"uuctl" | "systemd" => Ok(ProviderType::Uuctl),
|
||||
"weather" => Ok(ProviderType::Weather),
|
||||
"web" | "websearch" | "search" => Ok(ProviderType::WebSearch),
|
||||
// Plugin types are prefixed with "plugin:" (e.g., "plugin:github-repos")
|
||||
@@ -273,12 +273,14 @@ impl ProviderManager {
|
||||
}
|
||||
|
||||
/// Add a dynamic provider (e.g., from a Lua plugin)
|
||||
#[allow(dead_code)]
|
||||
pub fn add_provider(&mut self, provider: Box<dyn Provider>) {
|
||||
info!("Added plugin provider: {}", provider.name());
|
||||
self.providers.push(provider);
|
||||
}
|
||||
|
||||
/// Add multiple providers at once (for batch plugin loading)
|
||||
#[allow(dead_code)]
|
||||
pub fn add_providers(&mut self, providers: Vec<Box<dyn Provider>>) {
|
||||
for provider in providers {
|
||||
self.add_provider(provider);
|
||||
@@ -394,6 +396,10 @@ impl ProviderManager {
|
||||
if filter.active_prefix().is_none() && query.is_empty() {
|
||||
// Widget priority scores based on type
|
||||
for provider in &self.widget_providers {
|
||||
// Skip if this provider type is filtered out
|
||||
if !filter.is_active(provider.provider_type()) {
|
||||
continue;
|
||||
}
|
||||
let base_score = match provider.type_id() {
|
||||
"weather" => 12000,
|
||||
"pomodoro" => 11500,
|
||||
@@ -407,9 +413,15 @@ impl ProviderManager {
|
||||
}
|
||||
|
||||
// Query dynamic providers (calculator, websearch, filesearch)
|
||||
// Each provider internally checks if the query matches its prefix
|
||||
// Only query if:
|
||||
// 1. Their specific filter is active (e.g., :file prefix or Files tab selected), OR
|
||||
// 2. No specific single-mode filter is active (showing all providers)
|
||||
if !query.is_empty() {
|
||||
for (provider_idx, provider) in self.dynamic_providers.iter().enumerate() {
|
||||
// Skip if this provider type is explicitly filtered out
|
||||
if !filter.is_active(provider.provider_type()) {
|
||||
continue;
|
||||
}
|
||||
let dynamic_results = provider.query(query);
|
||||
let base_score = match provider.type_id() {
|
||||
"calc" => 10000,
|
||||
|
||||
@@ -42,6 +42,16 @@ impl NativeProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the ProviderType for this native provider
|
||||
/// Maps type_id string to the appropriate ProviderType variant
|
||||
fn get_provider_type(&self) -> ProviderType {
|
||||
// Parse type_id to get the proper ProviderType
|
||||
// This uses the FromStr impl which maps strings like "clipboard" -> ProviderType::Clipboard
|
||||
self.info.type_id.as_str().parse().unwrap_or_else(|_| {
|
||||
ProviderType::Plugin(self.info.type_id.to_string())
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert a plugin API item to a core LaunchItem
|
||||
fn convert_item(&self, item: ApiPluginItem) -> LaunchItem {
|
||||
LaunchItem {
|
||||
@@ -49,7 +59,7 @@ impl NativeProvider {
|
||||
name: item.name.to_string(),
|
||||
description: item.description.as_ref().map(|s| s.to_string()).into(),
|
||||
icon: item.icon.as_ref().map(|s| s.to_string()).into(),
|
||||
provider: ProviderType::Plugin(self.info.type_id.to_string()),
|
||||
provider: self.get_provider_type(),
|
||||
command: item.command.to_string(),
|
||||
terminal: item.terminal,
|
||||
tags: item.keywords.iter().map(|s| s.to_string()).collect(),
|
||||
@@ -113,7 +123,7 @@ impl Provider for NativeProvider {
|
||||
}
|
||||
|
||||
fn provider_type(&self) -> ProviderType {
|
||||
ProviderType::Plugin(self.info.type_id.to_string())
|
||||
self.get_provider_type()
|
||||
}
|
||||
|
||||
fn refresh(&mut self) {
|
||||
|
||||
@@ -67,6 +67,18 @@
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Symbolic icons - inherit text color */
|
||||
.owlry-symbolic-icon {
|
||||
-gtk-icon-style: symbolic;
|
||||
}
|
||||
|
||||
/* Emoji icon - displayed as large text */
|
||||
.owlry-emoji-icon {
|
||||
font-size: 24px;
|
||||
min-width: 32px;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
/* Result name */
|
||||
.owlry-result-name {
|
||||
font-size: var(--owlry-font-size, 14px);
|
||||
|
||||
@@ -72,6 +72,17 @@ pub fn generate_variables_css(config: &AppearanceConfig) -> String {
|
||||
css.push_str(&format!(" --owlry-badge-web: {};\n", badge_web));
|
||||
}
|
||||
|
||||
// Widget badge colors
|
||||
if let Some(ref badge_media) = config.colors.badge_media {
|
||||
css.push_str(&format!(" --owlry-badge-media: {};\n", badge_media));
|
||||
}
|
||||
if let Some(ref badge_weather) = config.colors.badge_weather {
|
||||
css.push_str(&format!(" --owlry-badge-weather: {};\n", badge_weather));
|
||||
}
|
||||
if let Some(ref badge_pomo) = config.colors.badge_pomo {
|
||||
css.push_str(&format!(" --owlry-badge-pomo: {};\n", badge_pomo));
|
||||
}
|
||||
|
||||
css.push_str("}\n");
|
||||
css
|
||||
}
|
||||
|
||||
@@ -35,6 +35,19 @@ struct SubmenuState {
|
||||
saved_search: String,
|
||||
}
|
||||
|
||||
/// State for lazy loading results
|
||||
#[derive(Default)]
|
||||
struct LazyLoadState {
|
||||
/// All matching results (may be more than displayed)
|
||||
all_results: Vec<LaunchItem>,
|
||||
/// Number of items currently displayed
|
||||
displayed_count: usize,
|
||||
}
|
||||
|
||||
/// Number of items to display initially and per batch
|
||||
const INITIAL_RESULTS: usize = 15;
|
||||
const LOAD_MORE_BATCH: usize = 10;
|
||||
|
||||
pub struct MainWindow {
|
||||
window: ApplicationWindow,
|
||||
search_entry: Entry,
|
||||
@@ -51,6 +64,11 @@ pub struct MainWindow {
|
||||
submenu_state: Rc<RefCell<SubmenuState>>,
|
||||
/// Parsed tab config (ProviderTypes for cycling)
|
||||
tab_order: Rc<Vec<ProviderType>>,
|
||||
/// Custom prompt text (overrides dynamic placeholder when set)
|
||||
#[allow(dead_code)]
|
||||
custom_prompt: Option<String>,
|
||||
/// Lazy loading state
|
||||
lazy_state: Rc<RefCell<LazyLoadState>>,
|
||||
}
|
||||
|
||||
impl MainWindow {
|
||||
@@ -60,6 +78,7 @@ impl MainWindow {
|
||||
providers: Rc<RefCell<ProviderManager>>,
|
||||
frecency: Rc<RefCell<FrecencyStore>>,
|
||||
filter: Rc<RefCell<ProviderFilter>>,
|
||||
custom_prompt: Option<String>,
|
||||
) -> Self {
|
||||
let cfg = config.borrow();
|
||||
|
||||
@@ -111,24 +130,21 @@ impl MainWindow {
|
||||
.build();
|
||||
filter_tabs.add_css_class("owlry-filter-tabs");
|
||||
|
||||
// Parse tabs config to ProviderTypes
|
||||
let tab_order: Vec<ProviderType> = cfg
|
||||
.general
|
||||
.tabs
|
||||
.iter()
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
let tab_order = Rc::new(tab_order);
|
||||
// Get enabled providers from filter (which respects CLI --mode/--providers or config)
|
||||
// This makes tabs dynamic based on what's actually enabled
|
||||
let enabled = filter.borrow().enabled_providers();
|
||||
let tab_strings: Vec<String> = enabled.iter().map(|p| p.to_string()).collect();
|
||||
let tab_order = Rc::new(enabled);
|
||||
|
||||
// Create toggle buttons for each provider (from config)
|
||||
let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter, &cfg.general.tabs);
|
||||
// Create toggle buttons for each enabled provider
|
||||
let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter, &tab_strings);
|
||||
let filter_buttons = Rc::new(RefCell::new(filter_buttons));
|
||||
|
||||
header_box.append(&mode_label);
|
||||
header_box.append(&filter_tabs);
|
||||
|
||||
// Search entry with dynamic placeholder
|
||||
let placeholder = Self::build_placeholder(&filter.borrow());
|
||||
// Search entry with dynamic placeholder (or custom prompt if provided)
|
||||
let placeholder = custom_prompt.clone().unwrap_or_else(|| Self::build_placeholder(&filter.borrow()));
|
||||
let search_entry = Entry::builder()
|
||||
.placeholder_text(&placeholder)
|
||||
.hexpand(true)
|
||||
@@ -175,6 +191,8 @@ impl MainWindow {
|
||||
|
||||
drop(cfg);
|
||||
|
||||
let lazy_state = Rc::new(RefCell::new(LazyLoadState::default()));
|
||||
|
||||
let main_window = Self {
|
||||
window,
|
||||
search_entry,
|
||||
@@ -190,9 +208,12 @@ impl MainWindow {
|
||||
filter_buttons,
|
||||
submenu_state: Rc::new(RefCell::new(SubmenuState::default())),
|
||||
tab_order,
|
||||
custom_prompt,
|
||||
lazy_state,
|
||||
};
|
||||
|
||||
main_window.setup_signals();
|
||||
main_window.setup_lazy_loading();
|
||||
main_window.update_results("");
|
||||
|
||||
// Ensure search entry has focus when window is shown
|
||||
@@ -232,11 +253,11 @@ impl MainWindow {
|
||||
|
||||
let mut results = current_results_for_auto.borrow_mut();
|
||||
for type_id in &widget_ids {
|
||||
if let Some(new_item) = providers_for_auto.borrow().get_widget_item(type_id) {
|
||||
if let Some(existing) = results.iter_mut().find(|i| i.id == new_item.id) {
|
||||
existing.name = new_item.name;
|
||||
existing.description = new_item.description;
|
||||
}
|
||||
if let Some(new_item) = providers_for_auto.borrow().get_widget_item(type_id)
|
||||
&& let Some(existing) = results.iter_mut().find(|i| i.id == new_item.id)
|
||||
{
|
||||
existing.name = new_item.name;
|
||||
existing.description = new_item.description;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -263,11 +284,21 @@ impl MainWindow {
|
||||
}
|
||||
};
|
||||
|
||||
let label = Self::provider_tab_label(&provider_type);
|
||||
let base_label = Self::provider_tab_label(&provider_type);
|
||||
// Show number hint in the label for first 9 tabs (using superscript)
|
||||
let label = if idx < 9 {
|
||||
let superscript = match idx + 1 {
|
||||
1 => "¹", 2 => "²", 3 => "³", 4 => "⁴", 5 => "⁵",
|
||||
6 => "⁶", 7 => "⁷", 8 => "⁸", 9 => "⁹", _ => "",
|
||||
};
|
||||
format!("{}{}", base_label, superscript)
|
||||
} else {
|
||||
base_label.to_string()
|
||||
};
|
||||
let shortcut = format!("Ctrl+{}", idx + 1);
|
||||
|
||||
let button = ToggleButton::builder()
|
||||
.label(label)
|
||||
.label(&label)
|
||||
.tooltip_text(&shortcut)
|
||||
.active(filter.borrow().is_enabled(provider_type.clone()))
|
||||
.build();
|
||||
@@ -525,6 +556,7 @@ impl MainWindow {
|
||||
let mode_label = self.mode_label.clone();
|
||||
let search_entry_for_change = self.search_entry.clone();
|
||||
let submenu_state = self.submenu_state.clone();
|
||||
let lazy_state = self.lazy_state.clone();
|
||||
|
||||
self.search_entry.connect_changed(move |entry| {
|
||||
let raw_query = entry.text();
|
||||
@@ -623,11 +655,21 @@ impl MainWindow {
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Clear existing results
|
||||
while let Some(child) = results_list.first_child() {
|
||||
results_list.remove(&child);
|
||||
}
|
||||
|
||||
for item in &results {
|
||||
// Lazy loading: store all results but only display initial batch
|
||||
let initial_count = INITIAL_RESULTS.min(results.len());
|
||||
{
|
||||
let mut lazy = lazy_state.borrow_mut();
|
||||
lazy.all_results = results.clone();
|
||||
lazy.displayed_count = initial_count;
|
||||
}
|
||||
|
||||
// Display only initial batch
|
||||
for item in results.iter().take(initial_count) {
|
||||
let row = ResultRow::new(item);
|
||||
results_list.append(&row);
|
||||
}
|
||||
@@ -636,7 +678,8 @@ impl MainWindow {
|
||||
results_list.select_row(Some(&first_row));
|
||||
}
|
||||
|
||||
*current_results.borrow_mut() = results;
|
||||
// current_results holds only what's displayed (for selection/activation)
|
||||
*current_results.borrow_mut() = results.into_iter().take(initial_count).collect();
|
||||
});
|
||||
|
||||
// Entry activate signal (Enter key in search entry)
|
||||
@@ -847,6 +890,7 @@ impl MainWindow {
|
||||
// Ctrl+1-9 toggle specific providers based on tab order (only when not in submenu)
|
||||
Key::_1 | Key::_2 | Key::_3 | Key::_4 | Key::_5 |
|
||||
Key::_6 | Key::_7 | Key::_8 | Key::_9 if ctrl => {
|
||||
info!("[UI] Ctrl+number detected: {:?}", key);
|
||||
if !submenu_state.borrow().active {
|
||||
let idx = match key {
|
||||
Key::_1 => 0,
|
||||
@@ -860,7 +904,9 @@ impl MainWindow {
|
||||
Key::_9 => 8,
|
||||
_ => return gtk4::glib::Propagation::Proceed,
|
||||
};
|
||||
info!("[UI] Toggling tab at index {}", idx);
|
||||
if let Some(provider) = tab_order.get(idx) {
|
||||
info!("[UI] Found provider: {:?}", provider);
|
||||
Self::toggle_provider_button(
|
||||
provider.clone(),
|
||||
&filter,
|
||||
@@ -868,6 +914,8 @@ impl MainWindow {
|
||||
&search_entry,
|
||||
&mode_label,
|
||||
);
|
||||
} else {
|
||||
info!("[UI] No provider at index {}, tab_order len={}", idx, tab_order.len());
|
||||
}
|
||||
}
|
||||
gtk4::glib::Propagation::Stop
|
||||
@@ -955,25 +1003,65 @@ impl MainWindow {
|
||||
}
|
||||
|
||||
let current = filter.borrow().enabled_providers();
|
||||
let all_enabled = current.len() == tab_order.len();
|
||||
|
||||
let next = if current.len() == 1 {
|
||||
let idx = tab_order.iter().position(|p| p == ¤t[0]).unwrap_or(0);
|
||||
if forward {
|
||||
tab_order[(idx + 1) % tab_order.len()].clone()
|
||||
// Cycle: All -> Provider1 -> Provider2 -> ... -> ProviderN -> All
|
||||
// In "All" mode (all providers enabled), we go to first provider (forward) or last (backward)
|
||||
// In single-provider mode, we go to next provider or back to All at the boundary
|
||||
if all_enabled {
|
||||
// Currently showing all, go to first (forward) or last (backward) single provider
|
||||
let next = if forward {
|
||||
tab_order[0].clone()
|
||||
} else {
|
||||
tab_order[(idx + tab_order.len() - 1) % tab_order.len()].clone()
|
||||
tab_order[tab_order.len() - 1].clone()
|
||||
};
|
||||
{
|
||||
let mut f = filter.borrow_mut();
|
||||
f.set_single_mode(next.clone());
|
||||
}
|
||||
for (ptype, button) in buttons.borrow().iter() {
|
||||
button.set_active(ptype == &next);
|
||||
}
|
||||
} else if current.len() == 1 {
|
||||
let idx = tab_order.iter().position(|p| p == ¤t[0]).unwrap_or(0);
|
||||
let at_boundary = if forward { idx == tab_order.len() - 1 } else { idx == 0 };
|
||||
|
||||
if at_boundary {
|
||||
// At boundary, go back to "All" mode
|
||||
{
|
||||
let mut f = filter.borrow_mut();
|
||||
for provider in tab_order {
|
||||
f.enable(provider.clone());
|
||||
}
|
||||
}
|
||||
for (_, button) in buttons.borrow().iter() {
|
||||
button.set_active(true);
|
||||
}
|
||||
} else {
|
||||
// Move to next/previous provider
|
||||
let next = if forward {
|
||||
tab_order[idx + 1].clone()
|
||||
} else {
|
||||
tab_order[idx - 1].clone()
|
||||
};
|
||||
{
|
||||
let mut f = filter.borrow_mut();
|
||||
f.set_single_mode(next.clone());
|
||||
}
|
||||
for (ptype, button) in buttons.borrow().iter() {
|
||||
button.set_active(ptype == &next);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tab_order[0].clone()
|
||||
};
|
||||
|
||||
{
|
||||
let mut f = filter.borrow_mut();
|
||||
f.set_single_mode(next.clone());
|
||||
}
|
||||
|
||||
for (ptype, button) in buttons.borrow().iter() {
|
||||
button.set_active(ptype == &next);
|
||||
// Some but not all providers enabled - go to first provider
|
||||
let next = tab_order[0].clone();
|
||||
{
|
||||
let mut f = filter.borrow_mut();
|
||||
f.set_single_mode(next.clone());
|
||||
}
|
||||
for (ptype, button) in buttons.borrow().iter() {
|
||||
button.set_active(ptype == &next);
|
||||
}
|
||||
}
|
||||
|
||||
mode_label.set_label(filter.borrow().mode_display_name());
|
||||
@@ -1009,6 +1097,7 @@ impl MainWindow {
|
||||
let use_frecency = cfg.providers.frecency;
|
||||
drop(cfg);
|
||||
|
||||
// Fetch all matching results (up to max_results)
|
||||
let results: Vec<LaunchItem> = if use_frecency {
|
||||
self.providers
|
||||
.borrow_mut()
|
||||
@@ -1025,11 +1114,21 @@ impl MainWindow {
|
||||
.collect()
|
||||
};
|
||||
|
||||
// Clear existing results
|
||||
while let Some(child) = self.results_list.first_child() {
|
||||
self.results_list.remove(&child);
|
||||
}
|
||||
|
||||
for item in &results {
|
||||
// Store all results for lazy loading
|
||||
let initial_count = INITIAL_RESULTS.min(results.len());
|
||||
{
|
||||
let mut lazy = self.lazy_state.borrow_mut();
|
||||
lazy.all_results = results.clone();
|
||||
lazy.displayed_count = initial_count;
|
||||
}
|
||||
|
||||
// Display initial batch only
|
||||
for item in results.iter().take(initial_count) {
|
||||
let row = ResultRow::new(item);
|
||||
self.results_list.append(&row);
|
||||
}
|
||||
@@ -1038,7 +1137,74 @@ impl MainWindow {
|
||||
self.results_list.select_row(Some(&first_row));
|
||||
}
|
||||
|
||||
*self.current_results.borrow_mut() = results;
|
||||
// current_results holds what's currently displayed
|
||||
*self.current_results.borrow_mut() = results.into_iter().take(initial_count).collect();
|
||||
}
|
||||
|
||||
/// Set up lazy loading scroll detection
|
||||
fn setup_lazy_loading(&self) {
|
||||
let vadj = self.scrolled.vadjustment();
|
||||
let results_list = self.results_list.clone();
|
||||
let lazy_state = self.lazy_state.clone();
|
||||
let current_results = self.current_results.clone();
|
||||
|
||||
// Load more on scroll
|
||||
vadj.connect_value_changed(move |adj| {
|
||||
let value = adj.value();
|
||||
let upper = adj.upper();
|
||||
let page_size = adj.page_size();
|
||||
|
||||
// Load more when near bottom (within 50px)
|
||||
let near_bottom = upper > page_size && (value + page_size >= upper - 50.0);
|
||||
if near_bottom {
|
||||
Self::load_more_items(&lazy_state, &results_list, ¤t_results);
|
||||
}
|
||||
});
|
||||
|
||||
// Also load more when selecting rows near the end (keyboard navigation)
|
||||
let lazy_state2 = self.lazy_state.clone();
|
||||
let results_list2 = self.results_list.clone();
|
||||
let current_results2 = self.current_results.clone();
|
||||
|
||||
self.results_list.connect_row_selected(move |_, row| {
|
||||
if let Some(row) = row {
|
||||
let index = row.index();
|
||||
let lazy = lazy_state2.borrow();
|
||||
let displayed = lazy.displayed_count;
|
||||
let all_count = lazy.all_results.len();
|
||||
drop(lazy);
|
||||
|
||||
// Load more if within 3 items of the end
|
||||
if displayed < all_count && (index as usize) >= displayed.saturating_sub(3) {
|
||||
Self::load_more_items(&lazy_state2, &results_list2, ¤t_results2);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Load more items from lazy state
|
||||
fn load_more_items(
|
||||
lazy_state: &Rc<RefCell<LazyLoadState>>,
|
||||
results_list: &ListBox,
|
||||
current_results: &Rc<RefCell<Vec<LaunchItem>>>,
|
||||
) {
|
||||
let mut lazy = lazy_state.borrow_mut();
|
||||
let all_count = lazy.all_results.len();
|
||||
let displayed = lazy.displayed_count;
|
||||
|
||||
if displayed < all_count {
|
||||
// Load next batch
|
||||
let new_end = (displayed + LOAD_MORE_BATCH).min(all_count);
|
||||
for item in lazy.all_results[displayed..new_end].iter() {
|
||||
let row = ResultRow::new(item);
|
||||
results_list.append(&row);
|
||||
}
|
||||
lazy.displayed_count = new_end;
|
||||
|
||||
// Update current_results
|
||||
let mut current = current_results.borrow_mut();
|
||||
current.extend(lazy.all_results[displayed..new_end].iter().cloned());
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle item activation - returns true if window should close
|
||||
@@ -1075,7 +1241,8 @@ impl MainWindow {
|
||||
debug!("[UI] Launch details: terminal={}, provider={:?}", item.terminal, item.provider);
|
||||
|
||||
let cmd = if item.terminal {
|
||||
format!("{} -e {}", config.general.terminal_command, item.command)
|
||||
let terminal = config.general.terminal_command.as_deref().unwrap_or("xterm");
|
||||
format!("{} -e {}", terminal, item.command)
|
||||
} else {
|
||||
item.command.clone()
|
||||
};
|
||||
|
||||
@@ -7,6 +7,17 @@ pub struct ResultRow {
|
||||
row: ListBoxRow,
|
||||
}
|
||||
|
||||
/// Check if a string looks like an emoji (starts with a non-ASCII character
|
||||
/// and is very short - typically 1-4 chars for complex emojis with ZWJ)
|
||||
fn is_emoji_icon(s: &str) -> bool {
|
||||
if s.is_empty() {
|
||||
return false;
|
||||
}
|
||||
// Emojis are non-ASCII and typically very short (1-8 chars for complex ZWJ sequences)
|
||||
let first_char = s.chars().next().unwrap();
|
||||
!first_char.is_ascii() && s.chars().count() <= 8
|
||||
}
|
||||
|
||||
impl ResultRow {
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(item: &LaunchItem) -> ListBoxRow {
|
||||
@@ -26,46 +37,64 @@ impl ResultRow {
|
||||
.margin_end(12)
|
||||
.build();
|
||||
|
||||
// Icon - handle GResource paths, file paths, icon names, and fallbacks
|
||||
// Icon - handle GResource paths, file paths, icon names, emojis, and fallbacks
|
||||
let icon_widget: Widget = if let Some(icon_path) = &item.icon {
|
||||
let img = if icon_path.starts_with("/org/owlry/launcher/icons/") {
|
||||
if is_emoji_icon(icon_path) {
|
||||
// Emoji character - display as text label
|
||||
let emoji_label = Label::builder()
|
||||
.label(icon_path)
|
||||
.width_request(32)
|
||||
.height_request(32)
|
||||
.valign(gtk4::Align::Center)
|
||||
.halign(gtk4::Align::Center)
|
||||
.build();
|
||||
emoji_label.add_css_class("owlry-result-icon");
|
||||
emoji_label.add_css_class("owlry-emoji-icon");
|
||||
emoji_label.upcast()
|
||||
} else if icon_path.starts_with("/org/owlry/launcher/icons/") {
|
||||
// GResource path - load from bundled resources
|
||||
Image::from_resource(icon_path)
|
||||
let img = Image::from_resource(icon_path);
|
||||
img.set_pixel_size(32);
|
||||
img.add_css_class("owlry-result-icon");
|
||||
img.upcast()
|
||||
} else if icon_path.starts_with('/') {
|
||||
// Absolute file path
|
||||
Image::from_file(icon_path)
|
||||
let img = Image::from_file(icon_path);
|
||||
img.set_pixel_size(32);
|
||||
img.add_css_class("owlry-result-icon");
|
||||
img.upcast()
|
||||
} else {
|
||||
// Icon theme name
|
||||
Image::from_icon_name(icon_path)
|
||||
};
|
||||
img.set_pixel_size(32);
|
||||
img.add_css_class("owlry-result-icon");
|
||||
img.upcast()
|
||||
let img = Image::from_icon_name(icon_path);
|
||||
img.set_pixel_size(32);
|
||||
img.add_css_class("owlry-result-icon");
|
||||
img.upcast()
|
||||
}
|
||||
} else {
|
||||
// Default icon based on provider type
|
||||
// Default icon based on provider type (using symbolic icons for theme color support)
|
||||
let default_icon = match &item.provider {
|
||||
crate::providers::ProviderType::Application => "application-x-executable",
|
||||
crate::providers::ProviderType::Bookmarks => "user-bookmarks",
|
||||
crate::providers::ProviderType::Calculator => "accessories-calculator",
|
||||
crate::providers::ProviderType::Clipboard => "edit-paste",
|
||||
crate::providers::ProviderType::Command => "utilities-terminal",
|
||||
crate::providers::ProviderType::Application => "application-x-executable-symbolic",
|
||||
crate::providers::ProviderType::Bookmarks => "user-bookmarks-symbolic",
|
||||
crate::providers::ProviderType::Calculator => "accessories-calculator-symbolic",
|
||||
crate::providers::ProviderType::Clipboard => "edit-paste-symbolic",
|
||||
crate::providers::ProviderType::Command => "utilities-terminal-symbolic",
|
||||
crate::providers::ProviderType::Dmenu => "view-list-symbolic",
|
||||
crate::providers::ProviderType::Emoji => "face-smile",
|
||||
crate::providers::ProviderType::Files => "folder",
|
||||
crate::providers::ProviderType::Scripts => "application-x-executable",
|
||||
crate::providers::ProviderType::Ssh => "network-server",
|
||||
crate::providers::ProviderType::System => "system-shutdown",
|
||||
crate::providers::ProviderType::Uuctl => "system-run",
|
||||
crate::providers::ProviderType::WebSearch => "web-browser",
|
||||
// Widget providers now have icons set, but keep fallbacks
|
||||
crate::providers::ProviderType::Emoji => "face-smile-symbolic",
|
||||
crate::providers::ProviderType::Files => "folder-symbolic",
|
||||
crate::providers::ProviderType::Scripts => "application-x-executable-symbolic",
|
||||
crate::providers::ProviderType::Ssh => "network-server-symbolic",
|
||||
crate::providers::ProviderType::System => "system-shutdown-symbolic",
|
||||
crate::providers::ProviderType::Uuctl => "system-run-symbolic",
|
||||
crate::providers::ProviderType::WebSearch => "web-browser-symbolic",
|
||||
crate::providers::ProviderType::Weather => "weather-clear-symbolic",
|
||||
crate::providers::ProviderType::MediaPlayer => "media-playback-start-symbolic",
|
||||
crate::providers::ProviderType::Pomodoro => "alarm-symbolic",
|
||||
crate::providers::ProviderType::Plugin(_) => "application-x-addon",
|
||||
crate::providers::ProviderType::Plugin(_) => "application-x-addon-symbolic",
|
||||
};
|
||||
let img = Image::from_icon_name(default_icon);
|
||||
img.set_pixel_size(32);
|
||||
img.add_css_class("owlry-result-icon");
|
||||
img.add_css_class("owlry-symbolic-icon");
|
||||
img.upcast()
|
||||
};
|
||||
|
||||
|
||||
344
data/themes/apex-neon.css
Normal file
344
data/themes/apex-neon.css
Normal file
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* Owlry - Apex Neon Theme
|
||||
* "State over Decoration."
|
||||
*
|
||||
* A high-contrast dark theme built for focus and clinical clarity.
|
||||
* Color exists to signal STATE, not to decorate space.
|
||||
*
|
||||
* Author: S0wlz (Owlibou)
|
||||
*
|
||||
* ─────────────────────────────────────────────────────────────────
|
||||
* APEX DNA - Semantic Color Roles:
|
||||
*
|
||||
* RED is the Predator: Active intent, cursor, current location, critical errors
|
||||
* CYAN is Informational: Technical data, links, neutral highlights
|
||||
* PURPLE is Sacred: Root access, special modes, exceptional states
|
||||
* GREEN is Success: Completion, OK states, positive feedback
|
||||
* YELLOW is Warning: Caution, load states, attention needed
|
||||
*
|
||||
* Rule: If a UI element is not important, it does not glow.
|
||||
* ─────────────────────────────────────────────────────────────────
|
||||
*
|
||||
* Core Palette:
|
||||
* - Void Black: #050505 (absolute background)
|
||||
* - Dark Surface: #141414 (inputs, inactive elements)
|
||||
* - Light Surface: #262626 (separators, borders)
|
||||
* - Stark White: #ededed (primary text)
|
||||
* - Muted: #737373 (secondary text)
|
||||
* - Razor Red: #ff0044 (THE accent - focus, cursor, selection)
|
||||
* - Electric Cyan: #00eaff (info, links, technical)
|
||||
* - Sacred Purple: #9d00ff (special, root, elevated)
|
||||
* - Neon Green: #00ff99 (success, OK)
|
||||
* - Warning Yellow: #ffb700 (warning, caution)
|
||||
*
|
||||
* Bright Escalations:
|
||||
* - Alert Red: #ff8899 (distinguishable from cursor)
|
||||
* - Active Cyan: #5af3ff (active info)
|
||||
* - Active Green: #2bffb2 (active success)
|
||||
* - Urgent Yellow: #ffd24d (urgent warning)
|
||||
* - Elevated Purple:#c84dff (elevated special)
|
||||
*
|
||||
* Usage: Set theme = "apex-neon" in config.toml
|
||||
*/
|
||||
|
||||
:root {
|
||||
/* Core surfaces */
|
||||
--owlry-bg: #050505;
|
||||
--owlry-bg-secondary: #141414;
|
||||
--owlry-border: #262626;
|
||||
--owlry-text: #ededed;
|
||||
--owlry-text-secondary: #737373;
|
||||
|
||||
/* The Predator - primary accent */
|
||||
--owlry-accent: #ff0044;
|
||||
--owlry-accent-bright: #ff8899;
|
||||
|
||||
/* Provider badges - mapped to Apex semantics */
|
||||
--owlry-badge-app: #00eaff; /* Cyan: apps are informational */
|
||||
--owlry-badge-bookmark: #ffb700; /* Yellow: bookmarks need attention */
|
||||
--owlry-badge-calc: #ffd24d; /* Bright Yellow: calculator results */
|
||||
--owlry-badge-clip: #9d00ff; /* Purple: clipboard is special */
|
||||
--owlry-badge-cmd: #9d00ff; /* Purple: commands are elevated */
|
||||
--owlry-badge-dmenu: #00ff99; /* Green: dmenu is success/pipe */
|
||||
--owlry-badge-emoji: #c84dff; /* Bright Purple: emoji is special */
|
||||
--owlry-badge-file: #5af3ff; /* Bright Cyan: file search is active info */
|
||||
--owlry-badge-script: #2bffb2; /* Bright Green: scripts execute successfully */
|
||||
--owlry-badge-ssh: #00eaff; /* Cyan: SSH is technical/info */
|
||||
--owlry-badge-sys: #ff0044; /* Red: system actions are critical */
|
||||
--owlry-badge-uuctl: #ffb700; /* Yellow: uuctl requires attention */
|
||||
--owlry-badge-web: #00eaff; /* Cyan: web is informational */
|
||||
|
||||
/* Widget badges */
|
||||
--owlry-badge-media: #c84dff; /* Bright Purple: media is special */
|
||||
--owlry-badge-weather: #5af3ff; /* Bright Cyan: weather is active info */
|
||||
--owlry-badge-pomo: #ff8899; /* Alert Red: pomodoro demands attention */
|
||||
}
|
||||
|
||||
.owlry-main {
|
||||
background-color: rgba(5, 5, 5, 0.98);
|
||||
border: 1px solid rgba(38, 38, 38, 0.8);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.8),
|
||||
0 0 0 1px rgba(255, 0, 68, 0.1);
|
||||
}
|
||||
|
||||
.owlry-search {
|
||||
background-color: rgba(20, 20, 20, 0.9);
|
||||
border: 2px solid rgba(38, 38, 38, 0.8);
|
||||
color: var(--owlry-text);
|
||||
caret-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-search:focus {
|
||||
border-color: var(--owlry-accent);
|
||||
box-shadow: 0 0 0 2px rgba(255, 0, 68, 0.3);
|
||||
}
|
||||
|
||||
.owlry-result-row:hover {
|
||||
background-color: rgba(20, 20, 20, 0.8);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected {
|
||||
background-color: rgba(255, 0, 68, 0.15);
|
||||
border-left: 3px solid var(--owlry-accent);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-name {
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-result-icon {
|
||||
color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
/* Provider badges - styled per Apex semantics */
|
||||
.owlry-badge-app {
|
||||
background-color: rgba(0, 234, 255, 0.15);
|
||||
color: var(--owlry-badge-app);
|
||||
}
|
||||
|
||||
.owlry-badge-bookmark {
|
||||
background-color: rgba(255, 183, 0, 0.15);
|
||||
color: var(--owlry-badge-bookmark);
|
||||
}
|
||||
|
||||
.owlry-badge-calc {
|
||||
background-color: rgba(255, 210, 77, 0.15);
|
||||
color: var(--owlry-badge-calc);
|
||||
}
|
||||
|
||||
.owlry-badge-clip {
|
||||
background-color: rgba(157, 0, 255, 0.15);
|
||||
color: var(--owlry-badge-clip);
|
||||
}
|
||||
|
||||
.owlry-badge-cmd {
|
||||
background-color: rgba(157, 0, 255, 0.15);
|
||||
color: var(--owlry-badge-cmd);
|
||||
}
|
||||
|
||||
.owlry-badge-dmenu {
|
||||
background-color: rgba(0, 255, 153, 0.15);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
}
|
||||
|
||||
.owlry-badge-emoji {
|
||||
background-color: rgba(200, 77, 255, 0.15);
|
||||
color: var(--owlry-badge-emoji);
|
||||
}
|
||||
|
||||
.owlry-badge-file {
|
||||
background-color: rgba(90, 243, 255, 0.15);
|
||||
color: var(--owlry-badge-file);
|
||||
}
|
||||
|
||||
.owlry-badge-script {
|
||||
background-color: rgba(43, 255, 178, 0.15);
|
||||
color: var(--owlry-badge-script);
|
||||
}
|
||||
|
||||
.owlry-badge-ssh {
|
||||
background-color: rgba(0, 234, 255, 0.15);
|
||||
color: var(--owlry-badge-ssh);
|
||||
}
|
||||
|
||||
.owlry-badge-sys {
|
||||
background-color: rgba(255, 0, 68, 0.15);
|
||||
color: var(--owlry-badge-sys);
|
||||
}
|
||||
|
||||
.owlry-badge-uuctl {
|
||||
background-color: rgba(255, 183, 0, 0.15);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
}
|
||||
|
||||
.owlry-badge-web {
|
||||
background-color: rgba(0, 234, 255, 0.15);
|
||||
color: var(--owlry-badge-web);
|
||||
}
|
||||
|
||||
/* Widget badges */
|
||||
.owlry-badge-media {
|
||||
background-color: rgba(200, 77, 255, 0.15);
|
||||
color: var(--owlry-badge-media);
|
||||
}
|
||||
|
||||
.owlry-badge-weather {
|
||||
background-color: rgba(90, 243, 255, 0.15);
|
||||
color: var(--owlry-badge-weather);
|
||||
}
|
||||
|
||||
.owlry-badge-pomo {
|
||||
background-color: rgba(255, 136, 153, 0.15);
|
||||
color: var(--owlry-badge-pomo);
|
||||
}
|
||||
|
||||
/* Filter button - default uses The Predator */
|
||||
.owlry-filter-button:checked {
|
||||
background-color: rgba(255, 0, 68, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border-color: rgba(255, 0, 68, 0.5);
|
||||
}
|
||||
|
||||
/* Provider-specific filter buttons - follow Apex semantics */
|
||||
.owlry-filter-app:checked {
|
||||
background-color: rgba(0, 234, 255, 0.15);
|
||||
color: var(--owlry-badge-app);
|
||||
border-color: rgba(0, 234, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-bookmark:checked {
|
||||
background-color: rgba(255, 183, 0, 0.15);
|
||||
color: var(--owlry-badge-bookmark);
|
||||
border-color: rgba(255, 183, 0, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-calc:checked {
|
||||
background-color: rgba(255, 210, 77, 0.15);
|
||||
color: var(--owlry-badge-calc);
|
||||
border-color: rgba(255, 210, 77, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-clip:checked {
|
||||
background-color: rgba(157, 0, 255, 0.15);
|
||||
color: var(--owlry-badge-clip);
|
||||
border-color: rgba(157, 0, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-cmd:checked {
|
||||
background-color: rgba(157, 0, 255, 0.15);
|
||||
color: var(--owlry-badge-cmd);
|
||||
border-color: rgba(157, 0, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-dmenu:checked {
|
||||
background-color: rgba(0, 255, 153, 0.15);
|
||||
color: var(--owlry-badge-dmenu);
|
||||
border-color: rgba(0, 255, 153, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-emoji:checked {
|
||||
background-color: rgba(200, 77, 255, 0.15);
|
||||
color: var(--owlry-badge-emoji);
|
||||
border-color: rgba(200, 77, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-file:checked {
|
||||
background-color: rgba(90, 243, 255, 0.15);
|
||||
color: var(--owlry-badge-file);
|
||||
border-color: rgba(90, 243, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-script:checked {
|
||||
background-color: rgba(43, 255, 178, 0.15);
|
||||
color: var(--owlry-badge-script);
|
||||
border-color: rgba(43, 255, 178, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-ssh:checked {
|
||||
background-color: rgba(0, 234, 255, 0.15);
|
||||
color: var(--owlry-badge-ssh);
|
||||
border-color: rgba(0, 234, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-sys:checked {
|
||||
background-color: rgba(255, 0, 68, 0.15);
|
||||
color: var(--owlry-badge-sys);
|
||||
border-color: rgba(255, 0, 68, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-uuctl:checked {
|
||||
background-color: rgba(255, 183, 0, 0.15);
|
||||
color: var(--owlry-badge-uuctl);
|
||||
border-color: rgba(255, 183, 0, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-web:checked {
|
||||
background-color: rgba(0, 234, 255, 0.15);
|
||||
color: var(--owlry-badge-web);
|
||||
border-color: rgba(0, 234, 255, 0.5);
|
||||
}
|
||||
|
||||
/* Widget filter buttons */
|
||||
.owlry-filter-media:checked {
|
||||
background-color: rgba(200, 77, 255, 0.15);
|
||||
color: var(--owlry-badge-media);
|
||||
border-color: rgba(200, 77, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-weather:checked {
|
||||
background-color: rgba(90, 243, 255, 0.15);
|
||||
color: var(--owlry-badge-weather);
|
||||
border-color: rgba(90, 243, 255, 0.5);
|
||||
}
|
||||
|
||||
.owlry-filter-pomodoro:checked {
|
||||
background-color: rgba(255, 136, 153, 0.15);
|
||||
color: var(--owlry-badge-pomo);
|
||||
border-color: rgba(255, 136, 153, 0.5);
|
||||
}
|
||||
|
||||
/* Scrollbar - subtle in Void, The Predator on active */
|
||||
scrollbar slider {
|
||||
background-color: rgba(38, 38, 38, 0.8);
|
||||
}
|
||||
|
||||
scrollbar slider:hover {
|
||||
background-color: rgba(64, 64, 64, 0.9);
|
||||
}
|
||||
|
||||
scrollbar slider:active {
|
||||
background-color: var(--owlry-accent);
|
||||
}
|
||||
|
||||
/* Text selection - Apex Hard Rule: black text on red (target locked) */
|
||||
selection {
|
||||
background-color: var(--owlry-accent);
|
||||
color: #050505;
|
||||
}
|
||||
|
||||
/* Mode indicator - The Predator marks current mode */
|
||||
.owlry-mode-indicator {
|
||||
background-color: rgba(255, 0, 68, 0.2);
|
||||
color: var(--owlry-accent);
|
||||
border: 1px solid rgba(255, 0, 68, 0.3);
|
||||
}
|
||||
|
||||
/* Hints bar */
|
||||
.owlry-hints {
|
||||
border-top: 1px solid rgba(38, 38, 38, 0.8);
|
||||
}
|
||||
|
||||
.owlry-hints-label {
|
||||
color: var(--owlry-text-secondary);
|
||||
}
|
||||
|
||||
/* Tag badges in results */
|
||||
.owlry-tag-badge {
|
||||
background-color: rgba(38, 38, 38, 0.6);
|
||||
color: var(--owlry-text-secondary);
|
||||
}
|
||||
|
||||
.owlry-result-row:selected .owlry-tag-badge {
|
||||
background-color: rgba(255, 136, 153, 0.25);
|
||||
color: var(--owlry-accent-bright);
|
||||
}
|
||||
181
justfile
181
justfile
@@ -60,6 +60,10 @@ install-local:
|
||||
sudo mkdir -p /usr/lib/owlry/plugins
|
||||
sudo mkdir -p /usr/lib/owlry/runtimes
|
||||
|
||||
echo "Cleaning up stale files..."
|
||||
# Remove runtime files that may have ended up in plugins dir (from old installs)
|
||||
sudo rm -f /usr/lib/owlry/plugins/libowlry_lua.so /usr/lib/owlry/plugins/libowlry_rune.so
|
||||
|
||||
echo "Installing core binary..."
|
||||
sudo install -Dm755 target/release/owlry /usr/bin/owlry
|
||||
|
||||
@@ -152,6 +156,50 @@ bump-plugins new_version:
|
||||
git commit -m "chore(plugins): bump all plugins to {{new_version}}"
|
||||
echo "All plugins bumped to {{new_version}}"
|
||||
|
||||
# Bump meta-packages (no crate, just AUR version)
|
||||
bump-meta new_version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
for pkg in owlry-essentials owlry-tools owlry-widgets owlry-full; do
|
||||
file="aur/$pkg/PKGBUILD"
|
||||
old=$(grep '^pkgver=' "$file" | sed 's/pkgver=//')
|
||||
if [ "$old" != "{{new_version}}" ]; then
|
||||
echo "Bumping $pkg from $old to {{new_version}}"
|
||||
sed -i 's/^pkgver=.*/pkgver={{new_version}}/' "$file"
|
||||
(cd "aur/$pkg" && makepkg --printsrcinfo > .SRCINFO)
|
||||
fi
|
||||
done
|
||||
echo "Meta-packages bumped to {{new_version}}"
|
||||
|
||||
# Bump all non-core crates (plugins + runtimes) to same version
|
||||
bump-all new_version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
# Bump plugins
|
||||
for toml in crates/owlry-plugin-*/Cargo.toml; do
|
||||
crate=$(basename $(dirname "$toml"))
|
||||
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
if [ "$old" != "{{new_version}}" ]; then
|
||||
echo "Bumping $crate from $old to {{new_version}}"
|
||||
sed -i 's/^version = ".*"/version = "{{new_version}}"/' "$toml"
|
||||
fi
|
||||
done
|
||||
# Bump runtimes
|
||||
for toml in crates/owlry-lua/Cargo.toml crates/owlry-rune/Cargo.toml; do
|
||||
if [ -f "$toml" ]; then
|
||||
crate=$(basename $(dirname "$toml"))
|
||||
old=$(grep '^version' "$toml" | head -1 | sed 's/.*"\(.*\)"/\1/')
|
||||
if [ "$old" != "{{new_version}}" ]; then
|
||||
echo "Bumping $crate from $old to {{new_version}}"
|
||||
sed -i 's/^version = ".*"/version = "{{new_version}}"/' "$toml"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
cargo check --workspace
|
||||
git add crates/owlry-plugin-*/Cargo.toml crates/owlry-lua/Cargo.toml crates/owlry-rune/Cargo.toml Cargo.lock
|
||||
git commit -m "chore: bump all plugins and runtimes to {{new_version}}"
|
||||
echo "All plugins and runtimes bumped to {{new_version}}"
|
||||
|
||||
# Bump core version (usage: just bump 0.2.0)
|
||||
bump new_version:
|
||||
#!/usr/bin/env bash
|
||||
@@ -246,10 +294,13 @@ aur-update-pkg pkg:
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Determine crate name (strip owlry- prefix for meta-packages)
|
||||
url="https://somegit.dev/Owlibou/owlry"
|
||||
core_ver="{{version}}"
|
||||
|
||||
# Determine crate version
|
||||
case "{{pkg}}" in
|
||||
owlry-essentials|owlry-tools|owlry-widgets|owlry-full)
|
||||
# Meta-packages have no crate, use PKGBUILD version
|
||||
# Meta-packages have no crate, keep current version
|
||||
crate_ver=$(grep '^pkgver=' "$aur_dir/PKGBUILD" | sed 's/pkgver=//')
|
||||
;;
|
||||
*)
|
||||
@@ -264,14 +315,23 @@ aur-update-pkg pkg:
|
||||
esac
|
||||
|
||||
cd "$aur_dir"
|
||||
url="https://somegit.dev/Owlibou/owlry"
|
||||
|
||||
echo "Updating {{pkg}} PKGBUILD to version $crate_ver"
|
||||
echo "Updating {{pkg}} PKGBUILD:"
|
||||
echo " pkgver=$crate_ver"
|
||||
|
||||
sed -i "s/^pkgver=.*/pkgver=$crate_ver/" PKGBUILD
|
||||
sed -i 's/^pkgrel=.*/pkgrel=1/' PKGBUILD
|
||||
|
||||
# Update checksums for packages that download source
|
||||
if grep -q "^source=" PKGBUILD; then
|
||||
# Update _srcver for plugins/runtimes (they download from core version tag)
|
||||
if grep -q "^_srcver=" PKGBUILD; then
|
||||
echo " _srcver=$core_ver"
|
||||
sed -i "s/^_srcver=.*/_srcver=$core_ver/" PKGBUILD
|
||||
# Update checksum using core version
|
||||
echo "Updating checksums (from v$core_ver)..."
|
||||
b2sum=$(curl -sL "$url/archive/v$core_ver.tar.gz" | b2sum | cut -d' ' -f1)
|
||||
sed -i "s/^b2sums=.*/b2sums=('$b2sum')/" PKGBUILD
|
||||
elif grep -q "^source=" PKGBUILD; then
|
||||
# Core package uses pkgver for source
|
||||
echo "Updating checksums..."
|
||||
b2sum=$(curl -sL "$url/archive/v$crate_ver.tar.gz" | b2sum | cut -d' ' -f1)
|
||||
sed -i "s/^b2sums=.*/b2sums=('$b2sum')/" PKGBUILD
|
||||
@@ -281,9 +341,9 @@ aur-update-pkg pkg:
|
||||
echo "Generating .SRCINFO..."
|
||||
makepkg --printsrcinfo > .SRCINFO
|
||||
|
||||
git diff
|
||||
git diff --stat
|
||||
echo ""
|
||||
echo "{{pkg}} updated to $crate_ver. Run 'just aur-publish-pkg {{pkg}}' to publish."
|
||||
echo "{{pkg}} updated. Run 'just aur-publish-pkg {{pkg}}' to publish."
|
||||
|
||||
# Publish a specific AUR package
|
||||
aur-publish-pkg pkg:
|
||||
@@ -340,6 +400,16 @@ aur-publish-plugins:
|
||||
echo ""
|
||||
done
|
||||
|
||||
# Publish all meta-packages
|
||||
aur-publish-meta:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
for pkg in owlry-essentials owlry-tools owlry-widgets owlry-full; do
|
||||
echo "=== Publishing $pkg ==="
|
||||
just aur-publish-pkg "$pkg"
|
||||
done
|
||||
echo "All meta-packages published!"
|
||||
|
||||
# List all AUR packages with their versions
|
||||
aur-status:
|
||||
#!/usr/bin/env bash
|
||||
@@ -357,8 +427,61 @@ aur-status:
|
||||
fi
|
||||
done
|
||||
|
||||
# Full release workflow (bump + tag + aur)
|
||||
release-full new_version: (bump new_version)
|
||||
# Update ALL AUR packages (core + plugins + runtimes + meta)
|
||||
aur-update-all:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "=== Updating core ==="
|
||||
just aur-update
|
||||
echo ""
|
||||
echo "=== Updating plugins ==="
|
||||
for dir in aur/owlry-plugin-*/; do
|
||||
pkg=$(basename "$dir")
|
||||
echo "--- $pkg ---"
|
||||
just aur-update-pkg "$pkg"
|
||||
done
|
||||
echo ""
|
||||
echo "=== Updating runtimes ==="
|
||||
just aur-update-pkg owlry-lua
|
||||
just aur-update-pkg owlry-rune
|
||||
echo ""
|
||||
echo "=== Updating meta-packages ==="
|
||||
for pkg in owlry-essentials owlry-tools owlry-widgets owlry-full; do
|
||||
echo "--- $pkg ---"
|
||||
# Use subshell to avoid cd issues
|
||||
(cd "aur/$pkg" && makepkg --printsrcinfo > .SRCINFO)
|
||||
done
|
||||
echo ""
|
||||
echo "All AUR packages updated. Run 'just aur-publish-all' to publish."
|
||||
|
||||
# Publish ALL AUR packages
|
||||
aur-publish-all:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
echo "=== Publishing core ==="
|
||||
just aur-publish
|
||||
echo ""
|
||||
echo "=== Publishing plugins ==="
|
||||
for dir in aur/owlry-plugin-*/; do
|
||||
pkg=$(basename "$dir")
|
||||
echo "--- $pkg ---"
|
||||
just aur-publish-pkg "$pkg"
|
||||
done
|
||||
echo ""
|
||||
echo "=== Publishing runtimes ==="
|
||||
just aur-publish-pkg owlry-lua
|
||||
just aur-publish-pkg owlry-rune
|
||||
echo ""
|
||||
echo "=== Publishing meta-packages ==="
|
||||
for pkg in owlry-essentials owlry-tools owlry-widgets owlry-full; do
|
||||
echo "--- $pkg ---"
|
||||
just aur-publish-pkg "$pkg"
|
||||
done
|
||||
echo ""
|
||||
echo "All AUR packages published!"
|
||||
|
||||
# Full release workflow for core only (bump + tag + aur)
|
||||
release-core new_version: (bump new_version)
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
@@ -376,5 +499,41 @@ release-full new_version: (bump new_version)
|
||||
just aur-update
|
||||
|
||||
echo ""
|
||||
echo "Release v{{new_version}} prepared!"
|
||||
echo "Core release v{{new_version}} prepared!"
|
||||
echo "Review AUR changes, then run 'just aur-publish'"
|
||||
|
||||
# Full release workflow for everything (core + plugins + runtimes)
|
||||
# Usage: just release-all 0.5.0 0.3.0
|
||||
# First arg is core version, second is plugins/runtimes version
|
||||
release-all core_version plugin_version:
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "=== Bumping versions ==="
|
||||
just bump {{core_version}}
|
||||
just bump-all {{plugin_version}}
|
||||
|
||||
echo ""
|
||||
echo "=== Pushing to origin ==="
|
||||
git push
|
||||
|
||||
echo ""
|
||||
echo "=== Creating tag ==="
|
||||
just tag
|
||||
|
||||
echo "Waiting for tag to propagate..."
|
||||
sleep 2
|
||||
|
||||
echo ""
|
||||
echo "=== Updating all AUR packages ==="
|
||||
just aur-update-all
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo "Release prepared!"
|
||||
echo " Core: v{{core_version}}"
|
||||
echo " Plugins/Runtimes: v{{plugin_version}}"
|
||||
echo ""
|
||||
echo "Review changes with 'just aur-status'"
|
||||
echo "Then publish with 'just aur-publish-all'"
|
||||
echo "=========================================="
|
||||
|
||||
Reference in New Issue
Block a user