use crate::{
model::{settings, update},
view::{
article::TogglePlainText,
component::menu::{self as card, Menu as Open},
keymap,
leaf::Pane,
root::{
CloseProject, Cydonia, NewArticle, NewBoard, NewSession, NewSessionNext,
NewSessionWith, NewTable, NextEntry, OpenProject, OpenSettings, PrevEntry,
ToggleChanges, ToggleSidebar, ToggleTerminal,
},
},
};
use bezel::{
gpui::{
self, AnyElement, App, Context, Div, KeyBinding, Menu, MenuItem, OsAction, OwnedMenu,
OwnedMenuItem, Window, actions, prelude::*,
},
ui::{icons, input, menu::Item},
};
const NATIVE: bool = cfg!(target_os = "macos");
actions!(
cydonia,
[
CheckForUpdates,
CloseWindow,
Hide,
HideOthers,
Minimize,
Quit,
ShowAll,
ToggleFullScreen,
Zoom
]
);
pub fn bindings() -> Vec<KeyBinding> {
if !NATIVE {
return vec![
KeyBinding::new("ctrl-q", Quit, keymap::GLOBAL),
KeyBinding::new("ctrl-w", CloseWindow, keymap::GLOBAL),
KeyBinding::new("f11", ToggleFullScreen, None),
];
}
vec![
KeyBinding::new("cmd-q", Quit, None),
KeyBinding::new("cmd-h", Hide, None),
KeyBinding::new("alt-cmd-h", HideOthers, None),
KeyBinding::new("cmd-w", CloseWindow, None),
KeyBinding::new("cmd-m", Minimize, None),
KeyBinding::new("ctrl-cmd-f", ToggleFullScreen, None),
]
}
pub fn init(cx: &mut App) {
cx.on_action(|_: &Quit, cx: &mut App| cx.quit());
cx.on_action(|_: &Hide, cx: &mut App| cx.hide());
cx.on_action(|_: &HideOthers, cx: &mut App| cx.hide_other_apps());
cx.on_action(|_: &ShowAll, cx: &mut App| cx.unhide_other_apps());
cx.on_action(|_: &Minimize, cx: &mut App| front(cx, |window| window.minimize_window()));
cx.on_action(|_: &Zoom, cx: &mut App| front(cx, |window| window.zoom_window()));
cx.on_action(|_: &ToggleFullScreen, cx: &mut App| {
front(cx, |window| window.toggle_fullscreen())
});
cx.on_action(|_: &CloseWindow, cx: &mut App| front(cx, |window| window.remove_window()));
cx.on_action(|_: &OpenProject, cx: &mut App| {
workspace(cx, |this, window, cx| {
this.open_project_action(&OpenProject, window, cx)
})
});
cx.on_action(|_: &OpenSettings, cx: &mut App| {
workspace(cx, |this, window, cx| {
this.open_settings_action(&OpenSettings, window, cx)
})
});
if update::supported(cx)
&& let Some(updater) = update::of(cx)
{
cx.on_action(move |_: &CheckForUpdates, cx: &mut App| {
updater.update(cx, |updater, cx| updater.check(true, cx));
});
}
refresh(cx);
}
pub fn refresh(cx: &mut App) {
let menus = menus(cx);
cx.set_menus(menus);
}
fn menus(cx: &App) -> Vec<Menu> {
let mut app = Vec::new();
if update::supported(cx) {
app.push(MenuItem::action("Check for Updates…", CheckForUpdates));
app.push(MenuItem::separator());
}
app.extend([
MenuItem::action("Settings…", OpenSettings),
MenuItem::separator(),
]);
if NATIVE {
app.extend([
MenuItem::action("Hide cydonia", Hide),
MenuItem::action("Hide Others", HideOthers),
MenuItem::action("Show All", ShowAll),
MenuItem::separator(),
]);
}
app.push(MenuItem::action("Quit cydonia", Quit));
let file = file_menu();
vec![
Menu::new("cydonia").items(app),
Menu::new("File").items(file),
Menu::new("Edit").items([
MenuItem::os_action("Undo", input::Undo, OsAction::Undo),
MenuItem::os_action("Redo", input::Redo, OsAction::Redo),
MenuItem::separator(),
MenuItem::os_action("Cut", input::Cut, OsAction::Cut),
MenuItem::os_action("Copy", input::Copy, OsAction::Copy),
MenuItem::os_action("Paste", input::Paste, OsAction::Paste),
MenuItem::separator(),
MenuItem::os_action("Select All", input::SelectAll, OsAction::SelectAll),
]),
Menu::new("View").items([
MenuItem::action("Toggle Sidebar", ToggleSidebar),
MenuItem::action("Toggle Terminal", ToggleTerminal),
MenuItem::action("Toggle Right Panel", ToggleChanges),
MenuItem::action("Open Files", crate::view::root::OpenFiles),
MenuItem::action("Open Review", crate::view::root::OpenReview),
MenuItem::separator(),
MenuItem::action("Next Entry", NextEntry),
MenuItem::action("Previous Entry", PrevEntry),
MenuItem::separator(),
MenuItem::action("Plain Text", TogglePlainText),
MenuItem::action("Find Card", crate::view::board::FindCard),
MenuItem::separator(),
MenuItem::action("Enter Full Screen", ToggleFullScreen),
]),
Menu::new("Window").items([
MenuItem::action("Minimize", Minimize),
MenuItem::action("Zoom", Zoom),
]),
]
}
fn file_menu() -> Vec<MenuItem> {
let agents = settings::load()
.map(|settings| settings.agents)
.unwrap_or_default();
let mut file = vec![MenuItem::action("New Session", NewSession)];
if agents.len() > 1 {
file.push(MenuItem::submenu(Menu::new("New Session With").items(
agents.into_iter().map(|agent| {
MenuItem::action(agent.name.clone(), NewSessionWith { agent: agent.name })
}),
)));
file.push(MenuItem::action(
"New Session on Next Agent",
NewSessionNext,
));
}
file.extend([
MenuItem::separator(),
MenuItem::action("New Board", NewBoard),
MenuItem::action("New Article", NewArticle),
MenuItem::action("New Table", NewTable),
MenuItem::separator(),
MenuItem::action("Open Project…", OpenProject),
MenuItem::action("Close Project", CloseProject),
MenuItem::separator(),
MenuItem::action("Close Window", CloseWindow),
]);
file
}
fn front(cx: &mut App, f: impl FnOnce(&mut Window) + 'static) {
let Some(window) = cx.active_window() else {
return;
};
cx.defer(move |cx| {
let _ = window.update(cx, |_, window, _| f(window));
});
}
fn workspace(cx: &mut App, f: impl FnOnce(&mut Cydonia, &mut Window, &mut Context<Cydonia>)) {
let Some(handle) = cx
.windows()
.into_iter()
.find_map(|window| window.downcast::<Cydonia>())
else {
return;
};
let _ = handle.update(cx, |this, window, cx| {
window.activate_window();
f(this, window, cx);
});
}
impl Cydonia {
pub(crate) fn app_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Option<AnyElement> {
if NATIVE {
return None;
}
let card = (self.menu == Some(Open::App))
.then(|| cx.get_menus())
.flatten()
.map(|menus| {
let rows = menus
.into_iter()
.map(|menu| submenu(menu, window, cx))
.collect();
self.menu_card("app-menu-card", rows, window, cx)
});
Some(
self.menu_button("app-menu", None, icons::layout::Menu, Open::App, cx)
.children(card)
.into_any_element(),
)
}
pub(crate) fn commands(&self, root: Div, cx: &mut Context<Self>) -> Div {
let workspace = self.workspace.read(cx);
let features = &workspace.settings.features;
let (sessions, boards, tables) = (features.sessions, features.boards, features.tables);
let project = workspace.active.is_some();
let showing = self.showing(cx);
let entries = showing.is_some();
root.on_action(cx.listener(Self::toggle_sidebar_action))
.on_action(cx.listener(Self::open_project_action))
.on_action(cx.listener(Self::open_settings_action))
.when(project, |root| {
root.on_action(cx.listener(Self::close_project_action))
.on_action(cx.listener(Self::new_article_action))
.when(sessions, |root| {
root.on_action(cx.listener(Self::new_session_action))
.on_action(cx.listener(Self::new_session_with_action))
.on_action(cx.listener(Self::new_session_next_action))
})
.when(boards, |root| {
root.on_action(cx.listener(Self::new_board_action))
})
.when(tables, |root| {
root.on_action(cx.listener(Self::new_table_action))
})
})
.when(project, |root| {
root.on_action(cx.listener(Self::toggle_terminal))
})
.when(showing == Some(Pane::Article), |root| {
root.on_action(cx.listener(Self::toggle_plain_text))
})
.when(showing == Some(Pane::Board), |root| {
root.on_action(cx.listener(Self::find_card))
})
.when(entries, |root| {
root.on_action(cx.listener(Self::next_entry))
.on_action(cx.listener(Self::prev_entry))
})
}
}
fn submenu(menu: OwnedMenu, window: &Window, cx: &App) -> (Item, card::Act) {
let (items, acts): (Vec<Item>, Vec<card::Act>) = menu
.items
.into_iter()
.filter_map(|item| match item {
OwnedMenuItem::Separator => Some((Item::Separator, inert())),
OwnedMenuItem::Submenu(menu) => Some(submenu(menu, window, cx)),
OwnedMenuItem::SystemMenu(_) => None,
OwnedMenuItem::Action { name, action, .. } => {
let item = Item::action(name).with_shortcut(&*action, window);
let item = match window.is_action_available(&*action, cx) {
true => item,
false => item.disabled(),
};
let act: card::Act = Box::new(move |_, _, window, cx| {
window.dispatch_action(action.boxed_clone(), cx)
});
Some((item, act))
}
})
.unzip();
let act: card::Act = Box::new(move |this, path, window, cx| {
if let Some((&at, rest)) = path.split_first()
&& let Some(act) = acts.get(at)
{
act(this, rest, window, cx);
}
});
(Item::submenu(menu.name, items), act)
}
fn inert() -> card::Act {
Box::new(|_, _, _, _| {})
}