Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3b1ff03ff8 | |||
| e1fb63d6c4 | |||
| 33e2f9cb5e | |||
| 6b21602a07 | |||
| 4516865c21 | |||
| 4fbc7fc4c9 | |||
| 536c5c5012 | |||
| abd4df6939 | |||
| 43f7228be2 | |||
| a1b47b8ba0 | |||
| ccce9b8572 | |||
| ffb4c2f127 | |||
| cde599db03 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,3 +11,5 @@ aur/*/*.tar.gz
|
|||||||
aur/*/*.tar.xz
|
aur/*/*.tar.xz
|
||||||
aur/*/*.pkg.tar.*
|
aur/*/*.pkg.tar.*
|
||||||
# Keep PKGBUILD and .SRCINFO tracked
|
# 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]
|
[package]
|
||||||
name = "owlry-lua"
|
name = "owlry-lua"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-api"
|
name = "owlry-plugin-api"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-bookmarks"
|
name = "owlry-plugin-bookmarks"
|
||||||
version = "0.2.0"
|
version = "0.2.2"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -25,3 +25,7 @@ dirs = "5.0"
|
|||||||
# For parsing Chrome bookmarks JSON
|
# For parsing Chrome bookmarks JSON
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
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
|
//! Bookmarks Plugin for Owlry
|
||||||
//!
|
//!
|
||||||
//! A static provider that reads browser bookmarks from Chrome/Chromium.
|
//! A static provider that reads browser bookmarks from various browsers.
|
||||||
//! Firefox support would require the rusqlite crate for reading places.sqlite.
|
|
||||||
//!
|
//!
|
||||||
//! Supported browsers:
|
//! Supported browsers:
|
||||||
|
//! - Firefox (via places.sqlite using SQLx)
|
||||||
//! - Chrome
|
//! - Chrome
|
||||||
//! - Chromium
|
//! - Chromium
|
||||||
//! - Brave
|
//! - Brave
|
||||||
@@ -14,8 +14,14 @@ use owlry_plugin_api::{
|
|||||||
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
owlry_plugin, PluginInfo, PluginItem, ProviderHandle, ProviderInfo, ProviderKind, API_VERSION,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use sqlx::sqlite::SqlitePoolOptions;
|
||||||
|
use sqlx::Row;
|
||||||
use std::fs;
|
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
|
// Plugin metadata
|
||||||
const PLUGIN_ID: &str = "bookmarks";
|
const PLUGIN_ID: &str = "bookmarks";
|
||||||
@@ -27,17 +33,141 @@ const PLUGIN_DESCRIPTION: &str = "Browser bookmark search";
|
|||||||
const PROVIDER_ID: &str = "bookmarks";
|
const PROVIDER_ID: &str = "bookmarks";
|
||||||
const PROVIDER_NAME: &str = "Bookmarks";
|
const PROVIDER_NAME: &str = "Bookmarks";
|
||||||
const PROVIDER_PREFIX: &str = ":bm";
|
const PROVIDER_PREFIX: &str = ":bm";
|
||||||
const PROVIDER_ICON: &str = "web-browser";
|
const PROVIDER_ICON: &str = "user-bookmarks-symbolic";
|
||||||
const PROVIDER_TYPE_ID: &str = "bookmarks";
|
const PROVIDER_TYPE_ID: &str = "bookmarks";
|
||||||
|
|
||||||
/// Bookmarks provider state - holds cached items
|
/// Bookmarks provider state - holds cached items
|
||||||
struct BookmarksState {
|
struct BookmarksState {
|
||||||
|
/// Cached bookmark items (returned immediately on refresh)
|
||||||
items: Vec<PluginItem>,
|
items: Vec<PluginItem>,
|
||||||
|
/// Flag to prevent concurrent background loads
|
||||||
|
loading: Arc<AtomicBool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BookmarksState {
|
impl BookmarksState {
|
||||||
fn new() -> Self {
|
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> {
|
fn chromium_bookmark_paths() -> Vec<PathBuf> {
|
||||||
@@ -61,18 +191,87 @@ impl BookmarksState {
|
|||||||
paths
|
paths
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_bookmarks(&mut self) {
|
fn firefox_places_paths() -> Vec<PathBuf> {
|
||||||
self.items.clear();
|
let mut paths = Vec::new();
|
||||||
|
|
||||||
// Load Chrome/Chromium bookmarks
|
if let Some(home) = dirs::home_dir() {
|
||||||
for path in Self::chromium_bookmark_paths() {
|
let firefox_dir = home.join(".mozilla/firefox");
|
||||||
if path.exists() {
|
if firefox_dir.exists() {
|
||||||
self.read_chrome_bookmarks(&path);
|
// 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) {
|
let content = match fs::read_to_string(path) {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
@@ -83,29 +282,27 @@ impl BookmarksState {
|
|||||||
Err(_) => return,
|
Err(_) => return,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Process bookmark bar and other folders
|
|
||||||
if let Some(roots) = bookmarks.roots {
|
if let Some(roots) = bookmarks.roots {
|
||||||
if let Some(bar) = roots.bookmark_bar {
|
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 {
|
if let Some(other) = roots.other {
|
||||||
self.process_chrome_folder(&other);
|
Self::process_chrome_folder_static(&other, items);
|
||||||
}
|
}
|
||||||
if let Some(synced) = roots.synced {
|
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 {
|
if let Some(ref children) = folder.children {
|
||||||
for child in children {
|
for child in children {
|
||||||
match child.node_type.as_deref() {
|
match child.node_type.as_deref() {
|
||||||
Some("url") => {
|
Some("url") => {
|
||||||
if let Some(ref url) = child.url {
|
if let Some(ref url) = child.url {
|
||||||
let name = child.name.clone().unwrap_or_else(|| url.clone());
|
let name = child.name.clone().unwrap_or_else(|| url.clone());
|
||||||
|
items.push(
|
||||||
self.items.push(
|
|
||||||
PluginItem::new(
|
PluginItem::new(
|
||||||
format!("bookmark:{}", url),
|
format!("bookmark:{}", url),
|
||||||
name,
|
name,
|
||||||
@@ -113,19 +310,199 @@ impl BookmarksState {
|
|||||||
)
|
)
|
||||||
.with_description(url.clone())
|
.with_description(url.clone())
|
||||||
.with_icon(PROVIDER_ICON)
|
.with_icon(PROVIDER_ICON)
|
||||||
.with_keywords(vec!["bookmark".to_string(), "web".to_string()]),
|
.with_keywords(vec!["bookmark".to_string(), "chrome".to_string()]),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some("folder") => {
|
Some("folder") => {
|
||||||
// Recursively process subfolders
|
Self::process_chrome_folder_static(child, items);
|
||||||
self.process_chrome_folder(child);
|
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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
|
// Chrome bookmark JSON structures
|
||||||
@@ -241,6 +618,14 @@ mod tests {
|
|||||||
assert!(!paths.is_empty());
|
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]
|
#[test]
|
||||||
fn test_parse_chrome_bookmarks() {
|
fn test_parse_chrome_bookmarks() {
|
||||||
let json = r#"{
|
let json = r#"{
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-calculator"
|
name = "owlry-plugin-calculator"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-clipboard"
|
name = "owlry-plugin-clipboard"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-emoji"
|
name = "owlry-plugin-emoji"
|
||||||
version = "0.2.0"
|
version = "0.2.2"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -423,6 +423,7 @@ impl EmojiState {
|
|||||||
name.to_string(),
|
name.to_string(),
|
||||||
format!("printf '%s' '{}' | wl-copy", emoji),
|
format!("printf '%s' '{}' | wl-copy", emoji),
|
||||||
)
|
)
|
||||||
|
.with_icon(*emoji) // Use emoji character as icon
|
||||||
.with_description(format!("{} {}", emoji, keywords))
|
.with_description(format!("{} {}", emoji, keywords))
|
||||||
.with_keywords(vec![name.to_string(), keywords.to_string()]),
|
.with_keywords(vec![name.to_string(), keywords.to_string()]),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-filesearch"
|
name = "owlry-plugin-filesearch"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-media"
|
name = "owlry-plugin-media"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-pomodoro"
|
name = "owlry-plugin-pomodoro"
|
||||||
version = "0.2.0"
|
version = "0.2.2"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -57,31 +57,31 @@ impl PomodoroConfig {
|
|||||||
let config_content = config_path
|
let config_content = config_path
|
||||||
.and_then(|p| fs::read_to_string(p).ok());
|
.and_then(|p| fs::read_to_string(p).ok());
|
||||||
|
|
||||||
if let Some(content) = config_content {
|
if let Some(content) = config_content
|
||||||
if let Ok(toml) = content.parse::<toml::Table>() {
|
&& 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()) {
|
// Try [plugins.pomodoro] first (new format)
|
||||||
if let Some(pomodoro) = plugins.get("pomodoro").and_then(|v| v.as_table()) {
|
if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table())
|
||||||
return Self::from_toml_table(pomodoro);
|
&& let Some(pomodoro) = plugins.get("pomodoro").and_then(|v| v.as_table())
|
||||||
}
|
{
|
||||||
}
|
return Self::from_toml_table(pomodoro);
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback to [providers] section (old format)
|
// Fallback to [providers] section (old format)
|
||||||
if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) {
|
if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) {
|
||||||
let work_mins = providers
|
let work_mins = providers
|
||||||
.get("pomodoro_work_mins")
|
.get("pomodoro_work_mins")
|
||||||
.and_then(|v| v.as_integer())
|
.and_then(|v| v.as_integer())
|
||||||
.map(|v| v as u32)
|
.map(|v| v as u32)
|
||||||
.unwrap_or(DEFAULT_WORK_MINS);
|
.unwrap_or(DEFAULT_WORK_MINS);
|
||||||
|
|
||||||
let break_mins = providers
|
let break_mins = providers
|
||||||
.get("pomodoro_break_mins")
|
.get("pomodoro_break_mins")
|
||||||
.and_then(|v| v.as_integer())
|
.and_then(|v| v.as_integer())
|
||||||
.map(|v| v as u32)
|
.map(|v| v as u32)
|
||||||
.unwrap_or(DEFAULT_BREAK_MINS);
|
.unwrap_or(DEFAULT_BREAK_MINS);
|
||||||
|
|
||||||
return Self { work_mins, break_mins };
|
return Self { work_mins, break_mins };
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-scripts"
|
name = "owlry-plugin-scripts"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-ssh"
|
name = "owlry-plugin-ssh"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-system"
|
name = "owlry-plugin-system"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-systemd"
|
name = "owlry-plugin-systemd"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-weather"
|
name = "owlry-plugin-weather"
|
||||||
version = "0.2.0"
|
version = "0.2.2"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -82,41 +82,41 @@ impl WeatherConfig {
|
|||||||
let config_content = config_path
|
let config_content = config_path
|
||||||
.and_then(|p| fs::read_to_string(p).ok());
|
.and_then(|p| fs::read_to_string(p).ok());
|
||||||
|
|
||||||
if let Some(content) = config_content {
|
if let Some(content) = config_content
|
||||||
if let Ok(toml) = content.parse::<toml::Table>() {
|
&& 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()) {
|
// Try [plugins.weather] first (new format)
|
||||||
if let Some(weather) = plugins.get("weather").and_then(|v| v.as_table()) {
|
if let Some(plugins) = toml.get("plugins").and_then(|v| v.as_table())
|
||||||
return Self::from_toml_table(weather);
|
&& let Some(weather) = plugins.get("weather").and_then(|v| v.as_table())
|
||||||
}
|
{
|
||||||
}
|
return Self::from_toml_table(weather);
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback to [providers] section (old format)
|
// Fallback to [providers] section (old format)
|
||||||
if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) {
|
if let Some(providers) = toml.get("providers").and_then(|v| v.as_table()) {
|
||||||
let provider_str = providers
|
let provider_str = providers
|
||||||
.get("weather_provider")
|
.get("weather_provider")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("wttr.in");
|
.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
|
let api_key = providers
|
||||||
.get("weather_api_key")
|
.get("weather_api_key")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.map(String::from);
|
.map(String::from);
|
||||||
|
|
||||||
let location = providers
|
let location = providers
|
||||||
.get("weather_location")
|
.get("weather_location")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
return Self {
|
return Self {
|
||||||
provider,
|
provider,
|
||||||
api_key,
|
api_key,
|
||||||
location,
|
location,
|
||||||
};
|
};
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-plugin-websearch"
|
name = "owlry-plugin-websearch"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry-rune"
|
name = "owlry-rune"
|
||||||
version = "0.2.0"
|
version = "0.2.1"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.90"
|
rust-version = "1.90"
|
||||||
description = "Rune scripting runtime for owlry plugins"
|
description = "Rune scripting runtime for owlry plugins"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "owlry"
|
name = "owlry"
|
||||||
version = "0.4.0"
|
version = "0.4.2"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.90"
|
rust-version = "1.90"
|
||||||
description = "A lightweight, owl-themed application launcher for Wayland"
|
description = "A lightweight, owl-themed application launcher for Wayland"
|
||||||
|
|||||||
@@ -56,7 +56,10 @@ impl OwlryApp {
|
|||||||
let native_providers = Self::load_native_plugins(&config.borrow());
|
let native_providers = Self::load_native_plugins(&config.borrow());
|
||||||
|
|
||||||
// Create provider manager with native plugins
|
// Create provider manager with native plugins
|
||||||
|
#[cfg(feature = "lua")]
|
||||||
let mut provider_manager = ProviderManager::with_native_plugins(native_providers);
|
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)
|
// Load Lua plugins if enabled (requires lua feature)
|
||||||
#[cfg(feature = "lua")]
|
#[cfg(feature = "lua")]
|
||||||
@@ -75,7 +78,7 @@ impl OwlryApp {
|
|||||||
);
|
);
|
||||||
let filter = Rc::new(RefCell::new(filter));
|
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
|
// Set up layer shell for Wayland overlay behavior
|
||||||
window.init_layer_shell();
|
window.init_layer_shell();
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ pub struct CliArgs {
|
|||||||
#[arg(long, short = 'p', value_delimiter = ',', value_parser = parse_provider)]
|
#[arg(long, short = 'p', value_delimiter = ',', value_parser = parse_provider)]
|
||||||
pub providers: Option<Vec<ProviderType>>,
|
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)
|
/// Subcommand to run (if any)
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
pub command: Option<Command>,
|
pub command: Option<Command>,
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ use std::process::Command;
|
|||||||
|
|
||||||
use crate::paths;
|
use crate::paths;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
#[serde(default)]
|
||||||
pub general: GeneralConfig,
|
pub general: GeneralConfig,
|
||||||
|
#[serde(default)]
|
||||||
pub appearance: AppearanceConfig,
|
pub appearance: AppearanceConfig,
|
||||||
|
#[serde(default)]
|
||||||
pub providers: ProvidersConfig,
|
pub providers: ProvidersConfig,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub plugins: PluginsConfig,
|
pub plugins: PluginsConfig,
|
||||||
@@ -17,9 +20,13 @@ pub struct Config {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct GeneralConfig {
|
pub struct GeneralConfig {
|
||||||
|
#[serde(default = "default_true")]
|
||||||
pub show_icons: bool,
|
pub show_icons: bool,
|
||||||
|
#[serde(default = "default_max_results")]
|
||||||
pub max_results: usize,
|
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.
|
/// Launch wrapper command for app execution.
|
||||||
/// Examples: "uwsm app --", "hyprctl dispatch exec --", "systemd-run --user --"
|
/// Examples: "uwsm app --", "hyprctl dispatch exec --", "systemd-run --user --"
|
||||||
/// If None or empty, launches directly via sh -c
|
/// If None or empty, launches directly via sh -c
|
||||||
@@ -31,6 +38,22 @@ pub struct GeneralConfig {
|
|||||||
pub tabs: Vec<String>,
|
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> {
|
fn default_tabs() -> Vec<String> {
|
||||||
vec![
|
vec![
|
||||||
"app".to_string(),
|
"app".to_string(),
|
||||||
@@ -40,9 +63,10 @@ fn default_tabs() -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// User-customizable theme colors
|
/// 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)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
pub struct ThemeColors {
|
pub struct ThemeColors {
|
||||||
|
// Core colors
|
||||||
pub background: Option<String>,
|
pub background: Option<String>,
|
||||||
pub background_secondary: Option<String>,
|
pub background_secondary: Option<String>,
|
||||||
pub border: Option<String>,
|
pub border: Option<String>,
|
||||||
@@ -64,13 +88,21 @@ pub struct ThemeColors {
|
|||||||
pub badge_sys: Option<String>,
|
pub badge_sys: Option<String>,
|
||||||
pub badge_uuctl: Option<String>,
|
pub badge_uuctl: Option<String>,
|
||||||
pub badge_web: 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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct AppearanceConfig {
|
pub struct AppearanceConfig {
|
||||||
|
#[serde(default = "default_width")]
|
||||||
pub width: i32,
|
pub width: i32,
|
||||||
|
#[serde(default = "default_height")]
|
||||||
pub height: i32,
|
pub height: i32,
|
||||||
|
#[serde(default = "default_font_size")]
|
||||||
pub font_size: u32,
|
pub font_size: u32,
|
||||||
|
#[serde(default = "default_border_radius")]
|
||||||
pub border_radius: u32,
|
pub border_radius: u32,
|
||||||
/// Theme name: None = GTK default, "owl" = built-in owl theme
|
/// Theme name: None = GTK default, "owl" = built-in owl theme
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
@@ -80,10 +112,31 @@ pub struct AppearanceConfig {
|
|||||||
pub colors: ThemeColors,
|
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)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ProvidersConfig {
|
pub struct ProvidersConfig {
|
||||||
|
#[serde(default = "default_true")]
|
||||||
pub applications: bool,
|
pub applications: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
pub commands: bool,
|
pub commands: bool,
|
||||||
|
#[serde(default = "default_true")]
|
||||||
pub uuctl: bool,
|
pub uuctl: bool,
|
||||||
/// Enable calculator provider (= expression or calc expression)
|
/// Enable calculator provider (= expression or calc expression)
|
||||||
#[serde(default = "default_true")]
|
#[serde(default = "default_true")]
|
||||||
@@ -159,6 +212,36 @@ pub struct ProvidersConfig {
|
|||||||
pub pomodoro_break_mins: u32,
|
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
|
/// Configuration for plugins
|
||||||
///
|
///
|
||||||
/// Supports per-plugin configuration via `[plugins.<name>]` sections:
|
/// Supports per-plugin configuration via `[plugins.<name>]` sections:
|
||||||
@@ -450,57 +533,7 @@ fn command_exists(cmd: &str) -> bool {
|
|||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Config {
|
// Note: Config derives Default via #[derive(Default)] - all sub-structs have impl Default
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn config_path() -> Option<PathBuf> {
|
pub fn config_path() -> Option<PathBuf> {
|
||||||
@@ -517,23 +550,37 @@ impl Config {
|
|||||||
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
|
pub fn load() -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let path = Self::config_path().ok_or("Could not determine config path")?;
|
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");
|
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)?;
|
// Auto-detect launch wrapper if not configured
|
||||||
let mut config: Config = toml::from_str(&content)?;
|
if config.general.launch_wrapper.is_none() {
|
||||||
info!("Loaded config from {:?}", path);
|
config.general.launch_wrapper = detect_launch_wrapper();
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
|
|||||||
@@ -64,8 +64,17 @@ impl ProviderFilter {
|
|||||||
if config_providers.scripts {
|
if config_providers.scripts {
|
||||||
set.insert(ProviderType::Scripts);
|
set.insert(ProviderType::Scripts);
|
||||||
}
|
}
|
||||||
// Note: Files, Calculator, WebSearch are dynamic providers
|
// Dynamic providers: add to filter set so they work in "All" mode
|
||||||
// that don't need to be in the filter set - they're triggered by prefix
|
// 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
|
// Default to apps if nothing enabled
|
||||||
if set.is_empty() {
|
if set.is_empty() {
|
||||||
set.insert(ProviderType::Application);
|
set.insert(ProviderType::Application);
|
||||||
@@ -104,9 +113,11 @@ impl ProviderFilter {
|
|||||||
#[cfg(feature = "dev-logging")]
|
#[cfg(feature = "dev-logging")]
|
||||||
debug!("[Filter] Toggled OFF {:?}, enabled: {:?}", provider, self.enabled);
|
debug!("[Filter] Toggled OFF {:?}, enabled: {:?}", provider, self.enabled);
|
||||||
} else {
|
} else {
|
||||||
|
#[cfg(feature = "dev-logging")]
|
||||||
|
let provider_debug = format!("{:?}", provider);
|
||||||
self.enabled.insert(provider);
|
self.enabled.insert(provider);
|
||||||
#[cfg(feature = "dev-logging")]
|
#[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
|
/// Check if a plugin is compatible with the given owlry version
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn check_compatibility(manifest: &PluginManifest, owlry_version: &str) -> PluginResult<()> {
|
pub fn check_compatibility(manifest: &PluginManifest, owlry_version: &str) -> PluginResult<()> {
|
||||||
if !manifest.is_compatible_with(owlry_version) {
|
if !manifest.is_compatible_with(owlry_version) {
|
||||||
return Err(PluginError::VersionMismatch {
|
return Err(PluginError::VersionMismatch {
|
||||||
@@ -230,6 +231,7 @@ impl PluginManifest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this plugin is compatible with the given owlry version
|
/// Check if this plugin is compatible with the given owlry version
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn is_compatible_with(&self, owlry_version: &str) -> bool {
|
pub fn is_compatible_with(&self, owlry_version: &str) -> bool {
|
||||||
let req = match semver::VersionReq::parse(&self.plugin.owlry_version) {
|
let req = match semver::VersionReq::parse(&self.plugin.owlry_version) {
|
||||||
Ok(r) => r,
|
Ok(r) => r,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ pub use api::provider::{PluginItem, ProviderRegistration};
|
|||||||
#[allow(unused_imports)]
|
#[allow(unused_imports)]
|
||||||
pub use api::{ActionRegistration, HookEvent, ThemeRegistration};
|
pub use api::{ActionRegistration, HookEvent, ThemeRegistration};
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
pub use error::{PluginError, PluginResult};
|
pub use error::{PluginError, PluginResult};
|
||||||
|
|
||||||
#[cfg(feature = "lua")]
|
#[cfg(feature = "lua")]
|
||||||
|
|||||||
@@ -271,14 +271,16 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_lua_runtime_not_installed() {
|
fn test_lua_runtime_check_doesnt_panic() {
|
||||||
// In test environment, runtime shouldn't be installed
|
// Just verify the function runs without panicking
|
||||||
assert!(!lua_runtime_available());
|
// Result depends on whether runtime is installed
|
||||||
|
let _available = lua_runtime_available();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_rune_runtime_not_installed() {
|
fn test_rune_runtime_check_doesnt_panic() {
|
||||||
// In test environment, runtime shouldn't be installed
|
// Just verify the function runs without panicking
|
||||||
assert!(!rune_runtime_available());
|
// Result depends on whether runtime is installed
|
||||||
|
let _available = rune_runtime_available();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -80,13 +80,13 @@ impl std::str::FromStr for ProviderType {
|
|||||||
"cmd" | "command" | "commands" => Ok(ProviderType::Command),
|
"cmd" | "command" | "commands" => Ok(ProviderType::Command),
|
||||||
"dmenu" => Ok(ProviderType::Dmenu),
|
"dmenu" => Ok(ProviderType::Dmenu),
|
||||||
"emoji" | "emojis" => Ok(ProviderType::Emoji),
|
"emoji" | "emojis" => Ok(ProviderType::Emoji),
|
||||||
"file" | "files" | "find" => Ok(ProviderType::Files),
|
"file" | "files" | "find" | "filesearch" => Ok(ProviderType::Files),
|
||||||
"media" | "mpris" | "player" => Ok(ProviderType::MediaPlayer),
|
"media" | "mpris" | "player" => Ok(ProviderType::MediaPlayer),
|
||||||
"pomo" | "pomodoro" | "timer" => Ok(ProviderType::Pomodoro),
|
"pomo" | "pomodoro" | "timer" => Ok(ProviderType::Pomodoro),
|
||||||
"script" | "scripts" => Ok(ProviderType::Scripts),
|
"script" | "scripts" => Ok(ProviderType::Scripts),
|
||||||
"ssh" => Ok(ProviderType::Ssh),
|
"ssh" => Ok(ProviderType::Ssh),
|
||||||
"sys" | "system" | "power" => Ok(ProviderType::System),
|
"sys" | "system" | "power" => Ok(ProviderType::System),
|
||||||
"uuctl" => Ok(ProviderType::Uuctl),
|
"uuctl" | "systemd" => Ok(ProviderType::Uuctl),
|
||||||
"weather" => Ok(ProviderType::Weather),
|
"weather" => Ok(ProviderType::Weather),
|
||||||
"web" | "websearch" | "search" => Ok(ProviderType::WebSearch),
|
"web" | "websearch" | "search" => Ok(ProviderType::WebSearch),
|
||||||
// Plugin types are prefixed with "plugin:" (e.g., "plugin:github-repos")
|
// 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)
|
/// Add a dynamic provider (e.g., from a Lua plugin)
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn add_provider(&mut self, provider: Box<dyn Provider>) {
|
pub fn add_provider(&mut self, provider: Box<dyn Provider>) {
|
||||||
info!("Added plugin provider: {}", provider.name());
|
info!("Added plugin provider: {}", provider.name());
|
||||||
self.providers.push(provider);
|
self.providers.push(provider);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add multiple providers at once (for batch plugin loading)
|
/// Add multiple providers at once (for batch plugin loading)
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn add_providers(&mut self, providers: Vec<Box<dyn Provider>>) {
|
pub fn add_providers(&mut self, providers: Vec<Box<dyn Provider>>) {
|
||||||
for provider in providers {
|
for provider in providers {
|
||||||
self.add_provider(provider);
|
self.add_provider(provider);
|
||||||
@@ -394,6 +396,10 @@ impl ProviderManager {
|
|||||||
if filter.active_prefix().is_none() && query.is_empty() {
|
if filter.active_prefix().is_none() && query.is_empty() {
|
||||||
// Widget priority scores based on type
|
// Widget priority scores based on type
|
||||||
for provider in &self.widget_providers {
|
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() {
|
let base_score = match provider.type_id() {
|
||||||
"weather" => 12000,
|
"weather" => 12000,
|
||||||
"pomodoro" => 11500,
|
"pomodoro" => 11500,
|
||||||
@@ -407,9 +413,15 @@ impl ProviderManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Query dynamic providers (calculator, websearch, filesearch)
|
// 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() {
|
if !query.is_empty() {
|
||||||
for (provider_idx, provider) in self.dynamic_providers.iter().enumerate() {
|
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 dynamic_results = provider.query(query);
|
||||||
let base_score = match provider.type_id() {
|
let base_score = match provider.type_id() {
|
||||||
"calc" => 10000,
|
"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
|
/// Convert a plugin API item to a core LaunchItem
|
||||||
fn convert_item(&self, item: ApiPluginItem) -> LaunchItem {
|
fn convert_item(&self, item: ApiPluginItem) -> LaunchItem {
|
||||||
LaunchItem {
|
LaunchItem {
|
||||||
@@ -49,7 +59,7 @@ impl NativeProvider {
|
|||||||
name: item.name.to_string(),
|
name: item.name.to_string(),
|
||||||
description: item.description.as_ref().map(|s| s.to_string()).into(),
|
description: item.description.as_ref().map(|s| s.to_string()).into(),
|
||||||
icon: item.icon.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(),
|
command: item.command.to_string(),
|
||||||
terminal: item.terminal,
|
terminal: item.terminal,
|
||||||
tags: item.keywords.iter().map(|s| s.to_string()).collect(),
|
tags: item.keywords.iter().map(|s| s.to_string()).collect(),
|
||||||
@@ -113,7 +123,7 @@ impl Provider for NativeProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn provider_type(&self) -> ProviderType {
|
fn provider_type(&self) -> ProviderType {
|
||||||
ProviderType::Plugin(self.info.type_id.to_string())
|
self.get_provider_type()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn refresh(&mut self) {
|
fn refresh(&mut self) {
|
||||||
|
|||||||
@@ -67,6 +67,18 @@
|
|||||||
opacity: 1;
|
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 */
|
/* Result name */
|
||||||
.owlry-result-name {
|
.owlry-result-name {
|
||||||
font-size: var(--owlry-font-size, 14px);
|
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));
|
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.push_str("}\n");
|
||||||
css
|
css
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,19 @@ struct SubmenuState {
|
|||||||
saved_search: String,
|
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 {
|
pub struct MainWindow {
|
||||||
window: ApplicationWindow,
|
window: ApplicationWindow,
|
||||||
search_entry: Entry,
|
search_entry: Entry,
|
||||||
@@ -51,6 +64,11 @@ pub struct MainWindow {
|
|||||||
submenu_state: Rc<RefCell<SubmenuState>>,
|
submenu_state: Rc<RefCell<SubmenuState>>,
|
||||||
/// Parsed tab config (ProviderTypes for cycling)
|
/// Parsed tab config (ProviderTypes for cycling)
|
||||||
tab_order: Rc<Vec<ProviderType>>,
|
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 {
|
impl MainWindow {
|
||||||
@@ -60,6 +78,7 @@ impl MainWindow {
|
|||||||
providers: Rc<RefCell<ProviderManager>>,
|
providers: Rc<RefCell<ProviderManager>>,
|
||||||
frecency: Rc<RefCell<FrecencyStore>>,
|
frecency: Rc<RefCell<FrecencyStore>>,
|
||||||
filter: Rc<RefCell<ProviderFilter>>,
|
filter: Rc<RefCell<ProviderFilter>>,
|
||||||
|
custom_prompt: Option<String>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let cfg = config.borrow();
|
let cfg = config.borrow();
|
||||||
|
|
||||||
@@ -111,24 +130,21 @@ impl MainWindow {
|
|||||||
.build();
|
.build();
|
||||||
filter_tabs.add_css_class("owlry-filter-tabs");
|
filter_tabs.add_css_class("owlry-filter-tabs");
|
||||||
|
|
||||||
// Parse tabs config to ProviderTypes
|
// Get enabled providers from filter (which respects CLI --mode/--providers or config)
|
||||||
let tab_order: Vec<ProviderType> = cfg
|
// This makes tabs dynamic based on what's actually enabled
|
||||||
.general
|
let enabled = filter.borrow().enabled_providers();
|
||||||
.tabs
|
let tab_strings: Vec<String> = enabled.iter().map(|p| p.to_string()).collect();
|
||||||
.iter()
|
let tab_order = Rc::new(enabled);
|
||||||
.filter_map(|s| s.parse().ok())
|
|
||||||
.collect();
|
|
||||||
let tab_order = Rc::new(tab_order);
|
|
||||||
|
|
||||||
// Create toggle buttons for each provider (from config)
|
// Create toggle buttons for each enabled provider
|
||||||
let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter, &cfg.general.tabs);
|
let filter_buttons = Self::create_filter_buttons(&filter_tabs, &filter, &tab_strings);
|
||||||
let filter_buttons = Rc::new(RefCell::new(filter_buttons));
|
let filter_buttons = Rc::new(RefCell::new(filter_buttons));
|
||||||
|
|
||||||
header_box.append(&mode_label);
|
header_box.append(&mode_label);
|
||||||
header_box.append(&filter_tabs);
|
header_box.append(&filter_tabs);
|
||||||
|
|
||||||
// Search entry with dynamic placeholder
|
// Search entry with dynamic placeholder (or custom prompt if provided)
|
||||||
let placeholder = Self::build_placeholder(&filter.borrow());
|
let placeholder = custom_prompt.clone().unwrap_or_else(|| Self::build_placeholder(&filter.borrow()));
|
||||||
let search_entry = Entry::builder()
|
let search_entry = Entry::builder()
|
||||||
.placeholder_text(&placeholder)
|
.placeholder_text(&placeholder)
|
||||||
.hexpand(true)
|
.hexpand(true)
|
||||||
@@ -175,6 +191,8 @@ impl MainWindow {
|
|||||||
|
|
||||||
drop(cfg);
|
drop(cfg);
|
||||||
|
|
||||||
|
let lazy_state = Rc::new(RefCell::new(LazyLoadState::default()));
|
||||||
|
|
||||||
let main_window = Self {
|
let main_window = Self {
|
||||||
window,
|
window,
|
||||||
search_entry,
|
search_entry,
|
||||||
@@ -190,9 +208,12 @@ impl MainWindow {
|
|||||||
filter_buttons,
|
filter_buttons,
|
||||||
submenu_state: Rc::new(RefCell::new(SubmenuState::default())),
|
submenu_state: Rc::new(RefCell::new(SubmenuState::default())),
|
||||||
tab_order,
|
tab_order,
|
||||||
|
custom_prompt,
|
||||||
|
lazy_state,
|
||||||
};
|
};
|
||||||
|
|
||||||
main_window.setup_signals();
|
main_window.setup_signals();
|
||||||
|
main_window.setup_lazy_loading();
|
||||||
main_window.update_results("");
|
main_window.update_results("");
|
||||||
|
|
||||||
// Ensure search entry has focus when window is shown
|
// Ensure search entry has focus when window is shown
|
||||||
@@ -232,11 +253,11 @@ impl MainWindow {
|
|||||||
|
|
||||||
let mut results = current_results_for_auto.borrow_mut();
|
let mut results = current_results_for_auto.borrow_mut();
|
||||||
for type_id in &widget_ids {
|
for type_id in &widget_ids {
|
||||||
if let Some(new_item) = providers_for_auto.borrow().get_widget_item(type_id) {
|
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) {
|
&& let Some(existing) = results.iter_mut().find(|i| i.id == new_item.id)
|
||||||
existing.name = new_item.name;
|
{
|
||||||
existing.description = new_item.description;
|
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 shortcut = format!("Ctrl+{}", idx + 1);
|
||||||
|
|
||||||
let button = ToggleButton::builder()
|
let button = ToggleButton::builder()
|
||||||
.label(label)
|
.label(&label)
|
||||||
.tooltip_text(&shortcut)
|
.tooltip_text(&shortcut)
|
||||||
.active(filter.borrow().is_enabled(provider_type.clone()))
|
.active(filter.borrow().is_enabled(provider_type.clone()))
|
||||||
.build();
|
.build();
|
||||||
@@ -525,6 +556,7 @@ impl MainWindow {
|
|||||||
let mode_label = self.mode_label.clone();
|
let mode_label = self.mode_label.clone();
|
||||||
let search_entry_for_change = self.search_entry.clone();
|
let search_entry_for_change = self.search_entry.clone();
|
||||||
let submenu_state = self.submenu_state.clone();
|
let submenu_state = self.submenu_state.clone();
|
||||||
|
let lazy_state = self.lazy_state.clone();
|
||||||
|
|
||||||
self.search_entry.connect_changed(move |entry| {
|
self.search_entry.connect_changed(move |entry| {
|
||||||
let raw_query = entry.text();
|
let raw_query = entry.text();
|
||||||
@@ -623,11 +655,21 @@ impl MainWindow {
|
|||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Clear existing results
|
||||||
while let Some(child) = results_list.first_child() {
|
while let Some(child) = results_list.first_child() {
|
||||||
results_list.remove(&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);
|
let row = ResultRow::new(item);
|
||||||
results_list.append(&row);
|
results_list.append(&row);
|
||||||
}
|
}
|
||||||
@@ -636,7 +678,8 @@ impl MainWindow {
|
|||||||
results_list.select_row(Some(&first_row));
|
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)
|
// 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)
|
// 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::_1 | Key::_2 | Key::_3 | Key::_4 | Key::_5 |
|
||||||
Key::_6 | Key::_7 | Key::_8 | Key::_9 if ctrl => {
|
Key::_6 | Key::_7 | Key::_8 | Key::_9 if ctrl => {
|
||||||
|
info!("[UI] Ctrl+number detected: {:?}", key);
|
||||||
if !submenu_state.borrow().active {
|
if !submenu_state.borrow().active {
|
||||||
let idx = match key {
|
let idx = match key {
|
||||||
Key::_1 => 0,
|
Key::_1 => 0,
|
||||||
@@ -860,7 +904,9 @@ impl MainWindow {
|
|||||||
Key::_9 => 8,
|
Key::_9 => 8,
|
||||||
_ => return gtk4::glib::Propagation::Proceed,
|
_ => return gtk4::glib::Propagation::Proceed,
|
||||||
};
|
};
|
||||||
|
info!("[UI] Toggling tab at index {}", idx);
|
||||||
if let Some(provider) = tab_order.get(idx) {
|
if let Some(provider) = tab_order.get(idx) {
|
||||||
|
info!("[UI] Found provider: {:?}", provider);
|
||||||
Self::toggle_provider_button(
|
Self::toggle_provider_button(
|
||||||
provider.clone(),
|
provider.clone(),
|
||||||
&filter,
|
&filter,
|
||||||
@@ -868,6 +914,8 @@ impl MainWindow {
|
|||||||
&search_entry,
|
&search_entry,
|
||||||
&mode_label,
|
&mode_label,
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
info!("[UI] No provider at index {}, tab_order len={}", idx, tab_order.len());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
gtk4::glib::Propagation::Stop
|
gtk4::glib::Propagation::Stop
|
||||||
@@ -955,25 +1003,65 @@ impl MainWindow {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let current = filter.borrow().enabled_providers();
|
let current = filter.borrow().enabled_providers();
|
||||||
|
let all_enabled = current.len() == tab_order.len();
|
||||||
|
|
||||||
let next = if current.len() == 1 {
|
// Cycle: All -> Provider1 -> Provider2 -> ... -> ProviderN -> All
|
||||||
let idx = tab_order.iter().position(|p| p == ¤t[0]).unwrap_or(0);
|
// In "All" mode (all providers enabled), we go to first provider (forward) or last (backward)
|
||||||
if forward {
|
// In single-provider mode, we go to next provider or back to All at the boundary
|
||||||
tab_order[(idx + 1) % tab_order.len()].clone()
|
if all_enabled {
|
||||||
|
// Currently showing all, go to first (forward) or last (backward) single provider
|
||||||
|
let next = if forward {
|
||||||
|
tab_order[0].clone()
|
||||||
} else {
|
} 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 {
|
} else {
|
||||||
tab_order[0].clone()
|
// Some but not all providers enabled - go to first provider
|
||||||
};
|
let next = tab_order[0].clone();
|
||||||
|
{
|
||||||
{
|
let mut f = filter.borrow_mut();
|
||||||
let mut f = filter.borrow_mut();
|
f.set_single_mode(next.clone());
|
||||||
f.set_single_mode(next.clone());
|
}
|
||||||
}
|
for (ptype, button) in buttons.borrow().iter() {
|
||||||
|
button.set_active(ptype == &next);
|
||||||
for (ptype, button) in buttons.borrow().iter() {
|
}
|
||||||
button.set_active(ptype == &next);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mode_label.set_label(filter.borrow().mode_display_name());
|
mode_label.set_label(filter.borrow().mode_display_name());
|
||||||
@@ -1009,6 +1097,7 @@ impl MainWindow {
|
|||||||
let use_frecency = cfg.providers.frecency;
|
let use_frecency = cfg.providers.frecency;
|
||||||
drop(cfg);
|
drop(cfg);
|
||||||
|
|
||||||
|
// Fetch all matching results (up to max_results)
|
||||||
let results: Vec<LaunchItem> = if use_frecency {
|
let results: Vec<LaunchItem> = if use_frecency {
|
||||||
self.providers
|
self.providers
|
||||||
.borrow_mut()
|
.borrow_mut()
|
||||||
@@ -1025,11 +1114,21 @@ impl MainWindow {
|
|||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Clear existing results
|
||||||
while let Some(child) = self.results_list.first_child() {
|
while let Some(child) = self.results_list.first_child() {
|
||||||
self.results_list.remove(&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);
|
let row = ResultRow::new(item);
|
||||||
self.results_list.append(&row);
|
self.results_list.append(&row);
|
||||||
}
|
}
|
||||||
@@ -1038,7 +1137,74 @@ impl MainWindow {
|
|||||||
self.results_list.select_row(Some(&first_row));
|
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
|
/// 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);
|
debug!("[UI] Launch details: terminal={}, provider={:?}", item.terminal, item.provider);
|
||||||
|
|
||||||
let cmd = if item.terminal {
|
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 {
|
} else {
|
||||||
item.command.clone()
|
item.command.clone()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ pub struct ResultRow {
|
|||||||
row: ListBoxRow,
|
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 {
|
impl ResultRow {
|
||||||
#[allow(clippy::new_ret_no_self)]
|
#[allow(clippy::new_ret_no_self)]
|
||||||
pub fn new(item: &LaunchItem) -> ListBoxRow {
|
pub fn new(item: &LaunchItem) -> ListBoxRow {
|
||||||
@@ -26,46 +37,64 @@ impl ResultRow {
|
|||||||
.margin_end(12)
|
.margin_end(12)
|
||||||
.build();
|
.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 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
|
// 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('/') {
|
} else if icon_path.starts_with('/') {
|
||||||
// Absolute file path
|
// 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 {
|
} else {
|
||||||
// Icon theme name
|
// Icon theme name
|
||||||
Image::from_icon_name(icon_path)
|
let img = Image::from_icon_name(icon_path);
|
||||||
};
|
img.set_pixel_size(32);
|
||||||
img.set_pixel_size(32);
|
img.add_css_class("owlry-result-icon");
|
||||||
img.add_css_class("owlry-result-icon");
|
img.upcast()
|
||||||
img.upcast()
|
}
|
||||||
} else {
|
} 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 {
|
let default_icon = match &item.provider {
|
||||||
crate::providers::ProviderType::Application => "application-x-executable",
|
crate::providers::ProviderType::Application => "application-x-executable-symbolic",
|
||||||
crate::providers::ProviderType::Bookmarks => "user-bookmarks",
|
crate::providers::ProviderType::Bookmarks => "user-bookmarks-symbolic",
|
||||||
crate::providers::ProviderType::Calculator => "accessories-calculator",
|
crate::providers::ProviderType::Calculator => "accessories-calculator-symbolic",
|
||||||
crate::providers::ProviderType::Clipboard => "edit-paste",
|
crate::providers::ProviderType::Clipboard => "edit-paste-symbolic",
|
||||||
crate::providers::ProviderType::Command => "utilities-terminal",
|
crate::providers::ProviderType::Command => "utilities-terminal-symbolic",
|
||||||
crate::providers::ProviderType::Dmenu => "view-list-symbolic",
|
crate::providers::ProviderType::Dmenu => "view-list-symbolic",
|
||||||
crate::providers::ProviderType::Emoji => "face-smile",
|
crate::providers::ProviderType::Emoji => "face-smile-symbolic",
|
||||||
crate::providers::ProviderType::Files => "folder",
|
crate::providers::ProviderType::Files => "folder-symbolic",
|
||||||
crate::providers::ProviderType::Scripts => "application-x-executable",
|
crate::providers::ProviderType::Scripts => "application-x-executable-symbolic",
|
||||||
crate::providers::ProviderType::Ssh => "network-server",
|
crate::providers::ProviderType::Ssh => "network-server-symbolic",
|
||||||
crate::providers::ProviderType::System => "system-shutdown",
|
crate::providers::ProviderType::System => "system-shutdown-symbolic",
|
||||||
crate::providers::ProviderType::Uuctl => "system-run",
|
crate::providers::ProviderType::Uuctl => "system-run-symbolic",
|
||||||
crate::providers::ProviderType::WebSearch => "web-browser",
|
crate::providers::ProviderType::WebSearch => "web-browser-symbolic",
|
||||||
// Widget providers now have icons set, but keep fallbacks
|
|
||||||
crate::providers::ProviderType::Weather => "weather-clear-symbolic",
|
crate::providers::ProviderType::Weather => "weather-clear-symbolic",
|
||||||
crate::providers::ProviderType::MediaPlayer => "media-playback-start-symbolic",
|
crate::providers::ProviderType::MediaPlayer => "media-playback-start-symbolic",
|
||||||
crate::providers::ProviderType::Pomodoro => "alarm-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);
|
let img = Image::from_icon_name(default_icon);
|
||||||
img.set_pixel_size(32);
|
img.set_pixel_size(32);
|
||||||
img.add_css_class("owlry-result-icon");
|
img.add_css_class("owlry-result-icon");
|
||||||
|
img.add_css_class("owlry-symbolic-icon");
|
||||||
img.upcast()
|
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/plugins
|
||||||
sudo mkdir -p /usr/lib/owlry/runtimes
|
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..."
|
echo "Installing core binary..."
|
||||||
sudo install -Dm755 target/release/owlry /usr/bin/owlry
|
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}}"
|
git commit -m "chore(plugins): bump all plugins to {{new_version}}"
|
||||||
echo "All plugins bumped 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 core version (usage: just bump 0.2.0)
|
||||||
bump new_version:
|
bump new_version:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
@@ -246,10 +294,13 @@ aur-update-pkg pkg:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
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
|
case "{{pkg}}" in
|
||||||
owlry-essentials|owlry-tools|owlry-widgets|owlry-full)
|
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=//')
|
crate_ver=$(grep '^pkgver=' "$aur_dir/PKGBUILD" | sed 's/pkgver=//')
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
@@ -264,14 +315,23 @@ aur-update-pkg pkg:
|
|||||||
esac
|
esac
|
||||||
|
|
||||||
cd "$aur_dir"
|
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/^pkgver=.*/pkgver=$crate_ver/" PKGBUILD
|
||||||
sed -i 's/^pkgrel=.*/pkgrel=1/' PKGBUILD
|
sed -i 's/^pkgrel=.*/pkgrel=1/' PKGBUILD
|
||||||
|
|
||||||
# Update checksums for packages that download source
|
# Update _srcver for plugins/runtimes (they download from core version tag)
|
||||||
if grep -q "^source=" PKGBUILD; then
|
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..."
|
echo "Updating checksums..."
|
||||||
b2sum=$(curl -sL "$url/archive/v$crate_ver.tar.gz" | b2sum | cut -d' ' -f1)
|
b2sum=$(curl -sL "$url/archive/v$crate_ver.tar.gz" | b2sum | cut -d' ' -f1)
|
||||||
sed -i "s/^b2sums=.*/b2sums=('$b2sum')/" PKGBUILD
|
sed -i "s/^b2sums=.*/b2sums=('$b2sum')/" PKGBUILD
|
||||||
@@ -281,9 +341,9 @@ aur-update-pkg pkg:
|
|||||||
echo "Generating .SRCINFO..."
|
echo "Generating .SRCINFO..."
|
||||||
makepkg --printsrcinfo > .SRCINFO
|
makepkg --printsrcinfo > .SRCINFO
|
||||||
|
|
||||||
git diff
|
git diff --stat
|
||||||
echo ""
|
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
|
# Publish a specific AUR package
|
||||||
aur-publish-pkg pkg:
|
aur-publish-pkg pkg:
|
||||||
@@ -340,6 +400,16 @@ aur-publish-plugins:
|
|||||||
echo ""
|
echo ""
|
||||||
done
|
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
|
# List all AUR packages with their versions
|
||||||
aur-status:
|
aur-status:
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
@@ -357,8 +427,61 @@ aur-status:
|
|||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Full release workflow (bump + tag + aur)
|
# Update ALL AUR packages (core + plugins + runtimes + meta)
|
||||||
release-full new_version: (bump new_version)
|
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
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
@@ -376,5 +499,41 @@ release-full new_version: (bump new_version)
|
|||||||
just aur-update
|
just aur-update
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Release v{{new_version}} prepared!"
|
echo "Core release v{{new_version}} prepared!"
|
||||||
echo "Review AUR changes, then run 'just aur-publish'"
|
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