mod about;
mod add_local;
mod add_project;
mod app;
mod banner;
mod busy;
mod context_menu;
mod deps;
#[cfg(target_os = "linux")]
mod desktop_entry;
mod detail;
mod drawer;
mod env_editor;
mod icons;
mod index_editor;
mod interpreter;
mod job;
mod members_editor;
mod preferences;
mod scripts_editor;
mod settings_editor;
mod shell;
mod shortcuts;
mod sidebar;
mod theme;
mod tray;
use bombadil_core::model::Config;
use bombadil_core::process::{CommandRunner, SystemCommandRunner};
use bombadil_core::store::{Loaded, Store, paths};
use std::sync::Arc;
const ICON_RGBA: &[u8] = include_bytes!("../assets/icon-64.rgba");
const ICON_SIDE: u32 = 64;
fn window_icon() -> Option<iced::window::Icon> {
iced::window::icon::from_rgba(ICON_RGBA.to_vec(), ICON_SIDE, ICON_SIDE).ok()
}
#[cfg(target_os = "linux")]
const APP_ID: &str = "bombadil";
fn window_settings(layout: &bombadil_core::model::Layout) -> iced::window::Settings {
let defaults = iced::window::Settings::default();
#[allow(unused_mut)]
let mut settings = iced::window::Settings {
icon: window_icon(),
size: app::initial_size(layout).unwrap_or(defaults.size),
exit_on_close_request: false,
..defaults
};
#[cfg(target_os = "linux")]
{
settings.platform_specific.application_id = APP_ID.to_string();
}
settings
}
static TRAY_COMMANDS: std::sync::OnceLock<
std::sync::Mutex<Option<std::sync::mpsc::Receiver<tray::TrayCommand>>>,
> = std::sync::OnceLock::new();
fn tray_subscription() -> iced::Subscription<app::Message> {
iced::Subscription::run_with((), |()| {
let (mut sender, receiver) = futures::channel::mpsc::channel(8);
let taken = TRAY_COMMANDS
.get()
.and_then(|slot| slot.lock().ok()?.take());
if let Some(commands) = taken {
std::thread::spawn(move || {
while let Ok(command) = commands.recv() {
if sender.try_send(app::Message::Tray(command)).is_err() {
break;
}
}
});
}
receiver
})
}
pub fn run() -> iced::Result {
#[cfg(target_os = "linux")]
desktop_entry::install(APP_ID);
let (loaded, persist) = load_config();
let window = window_settings(&loaded.config().settings.layout);
let runner: Arc<dyn CommandRunner> = Arc::new(SystemCommandRunner);
let tray = tray::start(ICON_RGBA, ICON_SIDE);
let tray_is_running = tray.is_some();
let tray = std::rc::Rc::new(tray.map(|(handle, commands)| {
let _ = TRAY_COMMANDS.set(std::sync::Mutex::new(Some(commands)));
handle
}));
iced::daemon(
move || {
let (mut state, task) = app::boot(loaded.clone());
state.window_settings = window.clone();
state.persist = persist.clone();
state.tray_is_running = tray_is_running;
let open = app::open_window(&state);
(state, iced::Task::batch([task, open]))
},
move |state: &mut app::App, message: app::Message| {
let moves_nothing = matches!(
message,
app::Message::Tick(_) | app::Message::SidebarDragged(_)
);
let task = app::update(state, message, runner.clone());
if !moves_nothing && let Some(tray) = tray.as_ref() {
let model = tray::menu_entries(&state.entries);
if model != state.tray_menu {
tray.set_menu(model.clone());
state.tray_menu = model;
}
}
task
},
view,
)
.title("Bombadil")
.subscription(|state: &app::App| {
iced::Subscription::batch([
iced::window::close_requests().map(|_| app::Message::CloseRequested),
tray_subscription(),
iced::system::theme_changes().map(app::Message::SystemThemeChanged),
iced::window::events().filter_map(window_event),
iced::event::listen_with(key_event),
if app::dragging(state) {
iced::event::listen_with(drag_event)
} else {
iced::Subscription::none()
},
if app::animating(state) {
iced::window::frames().map(app::Message::Tick)
} else {
iced::Subscription::none()
},
])
})
.font(theme::ATKINSON_REGULAR)
.font(theme::ATKINSON_SEMIBOLD)
.font(theme::PLEX_MONO_REGULAR)
.font(icons::FONT_BYTES)
.default_font(theme::FONT_PROSE)
.theme(|state: &app::App, _window| theme::theme(state.theme_mode()))
.run()
}
fn load_config() -> (Loaded, Arc<dyn app::ConfigPersist>) {
match paths::default_config_dir() {
Ok(dir) => {
let path = paths::config_file_in(&dir);
let (store, loaded) = Store::open(path);
(loaded, Arc::new(store))
}
Err(err) => (
Loaded::Unreadable {
config: Config::default(),
message: err.to_string(),
},
Arc::new(app::NullPersist),
),
}
}
fn key_event(
event: iced::Event,
status: iced::event::Status,
_window: iced::window::Id,
) -> Option<app::Message> {
match event {
iced::Event::Keyboard(iced::keyboard::Event::KeyPressed {
key,
physical_key,
modifiers,
repeat: false,
..
}) => {
shortcuts::shortcut(&key, physical_key, modifiers, status).map(app::Message::Shortcut)
}
_ => None,
}
}
fn drag_event(
event: iced::Event,
_status: iced::event::Status,
_window: iced::window::Id,
) -> Option<app::Message> {
match event {
iced::Event::Mouse(iced::mouse::Event::CursorMoved { position }) => {
Some(app::Message::SidebarDragged(position.x))
}
iced::Event::Mouse(iced::mouse::Event::ButtonReleased(iced::mouse::Button::Left)) => {
Some(app::Message::DragEnded)
}
_ => None,
}
}
fn view(state: &app::App, _window: iced::window::Id) -> iced::Element<'_, app::Message> {
app::view(state)
}
fn window_event((_id, event): (iced::window::Id, iced::window::Event)) -> Option<app::Message> {
match event {
iced::window::Event::Focused => Some(app::Message::WindowFocused),
iced::window::Event::Resized(size) => Some(app::Message::WindowResized(size)),
iced::window::Event::FileDropped(path) => Some(app::Message::FileDropped(path)),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use iced::keyboard::{Event, Key, Location, Modifiers, key};
fn ctrl_t(repeat: bool) -> iced::Event {
iced::Event::Keyboard(Event::KeyPressed {
key: Key::Character("t".into()),
modified_key: Key::Character("t".into()),
physical_key: key::Physical::Code(key::Code::KeyT),
location: Location::Standard,
modifiers: Modifiers::COMMAND,
text: None,
repeat,
})
}
#[test]
fn a_held_key_fires_once() {
let window = iced::window::Id::unique();
assert!(key_event(ctrl_t(false), iced::event::Status::Ignored, window).is_some());
assert!(key_event(ctrl_t(true), iced::event::Status::Ignored, window).is_none());
}
}