feat: Konsole parity phase 1b (layout save/load, SSH bookmarks)
- Serialize pane grid splits and per-tab state to RON - Save/load layout via cosmic-files dialogs with confirmation - SSH submenu from ~/.ssh/config Host entries (no wildcards) - i18n en/fr for layout and SSH menu entries
This commit is contained in:
parent
43d38aed8a
commit
d7cc5aaac1
5 changed files with 439 additions and 15 deletions
|
|
@ -115,6 +115,12 @@ new-tab = New tab
|
|||
new-window = New window
|
||||
profile = Profile
|
||||
menu-profiles = Profiles...
|
||||
ssh = SSH
|
||||
save-layout = Save layout...
|
||||
load-layout = Load layout...
|
||||
load-layout-title = Load layout
|
||||
load-layout-body = Loading a layout replaces all tabs and splits in this window.
|
||||
load = Load
|
||||
close-tab = Close tab
|
||||
quit = Quit
|
||||
|
||||
|
|
|
|||
|
|
@ -85,6 +85,12 @@ new-tab = Nouvel onglet
|
|||
new-window = Nouvelle fenêtre
|
||||
profile = Profil
|
||||
menu-profiles = Profils...
|
||||
ssh = SSH
|
||||
save-layout = Enregistrer la disposition...
|
||||
load-layout = Charger une disposition...
|
||||
load-layout-title = Charger une disposition
|
||||
load-layout-body = Charger une disposition remplace tous les onglets et divisions de cette fenêtre.
|
||||
load = Charger
|
||||
close-tab = Fermer l'onglet
|
||||
quit = Quitter
|
||||
|
||||
|
|
|
|||
147
src/layout.rs
Normal file
147
src/layout.rs
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
//! Serializable description of a window layout: the pane grid split tree
|
||||
//! and, for each pane, the list of tabs to restore.
|
||||
|
||||
use cosmic::widget::pane_grid;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
pub enum LayoutAxis {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
}
|
||||
|
||||
impl From<pane_grid::Axis> for LayoutAxis {
|
||||
fn from(axis: pane_grid::Axis) -> Self {
|
||||
match axis {
|
||||
pane_grid::Axis::Horizontal => Self::Horizontal,
|
||||
pane_grid::Axis::Vertical => Self::Vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LayoutAxis> for pane_grid::Axis {
|
||||
fn from(axis: LayoutAxis) -> Self {
|
||||
match axis {
|
||||
LayoutAxis::Horizontal => Self::Horizontal,
|
||||
LayoutAxis::Vertical => Self::Vertical,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize, Serialize)]
|
||||
pub struct LayoutTab {
|
||||
pub profile: Option<u64>,
|
||||
pub working_directory: Option<PathBuf>,
|
||||
pub title: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub enum LayoutNode {
|
||||
Split {
|
||||
axis: LayoutAxis,
|
||||
ratio: f32,
|
||||
a: Box<LayoutNode>,
|
||||
b: Box<LayoutNode>,
|
||||
},
|
||||
Pane {
|
||||
tabs: Vec<LayoutTab>,
|
||||
},
|
||||
}
|
||||
|
||||
impl LayoutNode {
|
||||
/// Leaf tab lists in depth-first order (a before b), matching the pane
|
||||
/// creation order of `pane_grid::State::with_configuration`.
|
||||
pub fn leaves(&self) -> Vec<&Vec<LayoutTab>> {
|
||||
let mut leaves = Vec::new();
|
||||
self.collect_leaves(&mut leaves);
|
||||
leaves
|
||||
}
|
||||
|
||||
fn collect_leaves<'a>(&'a self, leaves: &mut Vec<&'a Vec<LayoutTab>>) {
|
||||
match self {
|
||||
Self::Split { a, b, .. } => {
|
||||
a.collect_leaves(leaves);
|
||||
b.collect_leaves(leaves);
|
||||
}
|
||||
Self::Pane { tabs } => leaves.push(tabs),
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a pane grid configuration with one empty tab model per leaf.
|
||||
pub fn configuration<T, F: FnMut() -> T>(
|
||||
&self,
|
||||
new_model: &mut F,
|
||||
) -> pane_grid::Configuration<T> {
|
||||
match self {
|
||||
Self::Split { axis, ratio, a, b } => pane_grid::Configuration::Split {
|
||||
axis: (*axis).into(),
|
||||
ratio: *ratio,
|
||||
a: Box::new(a.configuration(new_model)),
|
||||
b: Box::new(b.configuration(new_model)),
|
||||
},
|
||||
Self::Pane { .. } => pane_grid::Configuration::Pane(new_model()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn sample() -> LayoutNode {
|
||||
LayoutNode::Split {
|
||||
axis: LayoutAxis::Vertical,
|
||||
ratio: 0.6,
|
||||
a: Box::new(LayoutNode::Pane {
|
||||
tabs: vec![
|
||||
LayoutTab {
|
||||
profile: Some(1),
|
||||
working_directory: Some(PathBuf::from("/tmp")),
|
||||
title: Some("build".to_string()),
|
||||
},
|
||||
LayoutTab::default(),
|
||||
],
|
||||
}),
|
||||
b: Box::new(LayoutNode::Split {
|
||||
axis: LayoutAxis::Horizontal,
|
||||
ratio: 0.5,
|
||||
a: Box::new(LayoutNode::Pane {
|
||||
tabs: vec![LayoutTab::default()],
|
||||
}),
|
||||
b: Box::new(LayoutNode::Pane {
|
||||
tabs: vec![LayoutTab::default()],
|
||||
}),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ron_round_trip() {
|
||||
let layout = sample();
|
||||
let ron = ron::ser::to_string_pretty(&layout, ron::ser::PrettyConfig::new()).unwrap();
|
||||
let parsed: LayoutNode = ron::from_str(&ron).unwrap();
|
||||
let leaves = parsed.leaves();
|
||||
assert_eq!(leaves.len(), 3);
|
||||
assert_eq!(leaves[0].len(), 2);
|
||||
assert_eq!(leaves[0][0].profile, Some(1));
|
||||
assert_eq!(leaves[0][0].title.as_deref(), Some("build"));
|
||||
assert_eq!(
|
||||
leaves[0][0].working_directory.as_deref(),
|
||||
Some(std::path::Path::new("/tmp"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_depth_first_order() {
|
||||
let layout = sample();
|
||||
// Depth-first: pane a first (2 tabs), then the two nested panes
|
||||
let leaves = layout.leaves();
|
||||
assert_eq!(
|
||||
leaves.iter().map(|tabs| tabs.len()).collect::<Vec<_>>(),
|
||||
vec![2, 1, 1]
|
||||
);
|
||||
}
|
||||
}
|
||||
252
src/main.rs
252
src/main.rs
|
|
@ -58,6 +58,9 @@ mod icon_cache;
|
|||
use key_bind::key_binds;
|
||||
mod key_bind;
|
||||
|
||||
use layout::{LayoutNode, LayoutTab};
|
||||
mod layout;
|
||||
|
||||
mod shortcuts;
|
||||
|
||||
mod localize;
|
||||
|
|
@ -116,6 +119,39 @@ fn monitor_update_tab_icon(
|
|||
}
|
||||
}
|
||||
|
||||
/// Concrete host names from `~/.ssh/config` (Host entries without wildcards
|
||||
/// or negations), sorted and deduplicated.
|
||||
fn ssh_config_hosts() -> Vec<String> {
|
||||
let Some(home) = env::var_os("HOME") else {
|
||||
return Vec::new();
|
||||
};
|
||||
let path = PathBuf::from(home).join(".ssh").join("config");
|
||||
let Ok(content) = fs::read_to_string(&path) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let mut hosts = BTreeSet::new();
|
||||
for line in content.lines() {
|
||||
let line = line.trim();
|
||||
if line.starts_with('#') {
|
||||
continue;
|
||||
}
|
||||
let mut parts = line.split_whitespace();
|
||||
let Some(keyword) = parts.next() else {
|
||||
continue;
|
||||
};
|
||||
if !keyword.eq_ignore_ascii_case("host") {
|
||||
continue;
|
||||
}
|
||||
for token in parts {
|
||||
if token.contains(['*', '?']) || token.starts_with('!') {
|
||||
continue;
|
||||
}
|
||||
hosts.insert(token.to_string());
|
||||
}
|
||||
}
|
||||
hosts.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Runs application with these settings
|
||||
#[rustfmt::skip]
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
|
|
@ -332,6 +368,8 @@ pub enum Action {
|
|||
Find,
|
||||
KeyboardShortcuts,
|
||||
LaunchUrlByMenu,
|
||||
LayoutLoad,
|
||||
LayoutSave,
|
||||
Monitor(MonitorKind),
|
||||
PaneFocusDown,
|
||||
PaneFocusLeft,
|
||||
|
|
@ -347,6 +385,7 @@ pub enum Action {
|
|||
SaveOutput,
|
||||
SelectAll,
|
||||
Settings,
|
||||
SshConnect(usize),
|
||||
#[cfg(feature = "password_manager")]
|
||||
PasswordManager,
|
||||
ShowHeaderBar(bool),
|
||||
|
|
@ -388,6 +427,8 @@ impl Action {
|
|||
Self::Find => Message::Find(true),
|
||||
Self::KeyboardShortcuts => Message::ToggleContextPage(ContextPage::KeyboardShortcuts),
|
||||
Self::LaunchUrlByMenu => Message::LaunchUrlByMenu,
|
||||
Self::LayoutLoad => Message::LayoutLoad,
|
||||
Self::LayoutSave => Message::LayoutSave,
|
||||
Self::Monitor(monitor_kind) => Message::MonitorToggle(*monitor_kind, entity_opt),
|
||||
Self::PaneFocusDown => Message::PaneFocusAdjacent(pane_grid::Direction::Down),
|
||||
Self::PaneFocusLeft => Message::PaneFocusAdjacent(pane_grid::Direction::Left),
|
||||
|
|
@ -405,6 +446,7 @@ impl Action {
|
|||
Self::SaveOutput => Message::SaveOutput(entity_opt),
|
||||
Self::SelectAll => Message::SelectAll(entity_opt),
|
||||
Self::Settings => Message::ToggleContextPage(ContextPage::Settings),
|
||||
Self::SshConnect(index) => Message::SshConnect(*index),
|
||||
Self::ShowHeaderBar(show_headerbar) => Message::ShowHeaderBar(*show_headerbar),
|
||||
Self::TabActivate0 => Message::TabActivateJump(0),
|
||||
Self::TabActivate1 => Message::TabActivateJump(1),
|
||||
|
|
@ -479,6 +521,12 @@ pub enum Message {
|
|||
Key(Modifiers, Physical, Key),
|
||||
LaunchUrl(String),
|
||||
LaunchUrlByMenu,
|
||||
LayoutLoad,
|
||||
LayoutLoadApply,
|
||||
LayoutLoadCancel,
|
||||
LayoutLoadResult(DialogResult),
|
||||
LayoutSave,
|
||||
LayoutSaveResult(DialogResult),
|
||||
Modifiers(Modifiers),
|
||||
ShortcutCaptureCancel,
|
||||
ShortcutCaptureStart(shortcuts::KeyBindAction),
|
||||
|
|
@ -522,6 +570,7 @@ pub enum Message {
|
|||
SelectAll(Option<segmented_button::Entity>),
|
||||
ShowAdvancedFontSettings(bool),
|
||||
ShowHeaderBar(bool),
|
||||
SshConnect(usize),
|
||||
SyntaxTheme(ColorSchemeKind, usize),
|
||||
SystemThemeChange,
|
||||
TabNewInheritWorkingDirectory(bool),
|
||||
|
|
@ -618,6 +667,8 @@ pub struct App {
|
|||
color_scheme_rename_id: widget::Id,
|
||||
rename_tab: Option<(segmented_button::Entity, String)>,
|
||||
rename_tab_id: widget::Id,
|
||||
layout_load_pending: Option<LayoutNode>,
|
||||
ssh_hosts: Vec<String>,
|
||||
color_scheme_tab_model: widget::segmented_button::SingleSelectModel,
|
||||
profile_expanded: Option<ProfileId>,
|
||||
show_advanced_font_settings: bool,
|
||||
|
|
@ -1702,6 +1753,97 @@ impl App {
|
|||
})
|
||||
}
|
||||
|
||||
/// Snapshot the current pane grid and tabs as a serializable layout.
|
||||
fn layout_from_state(&self, node: &pane_grid::Node) -> LayoutNode {
|
||||
match node {
|
||||
pane_grid::Node::Split {
|
||||
axis, ratio, a, b, ..
|
||||
} => LayoutNode::Split {
|
||||
axis: (*axis).into(),
|
||||
ratio: *ratio,
|
||||
a: Box::new(self.layout_from_state(a)),
|
||||
b: Box::new(self.layout_from_state(b)),
|
||||
},
|
||||
pane_grid::Node::Pane(pane) => {
|
||||
let mut tabs = Vec::new();
|
||||
if let Some(tab_model) = self.pane_model.panes.get(*pane) {
|
||||
for entity in tab_model.iter() {
|
||||
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
||||
let terminal = terminal.lock().unwrap();
|
||||
tabs.push(LayoutTab {
|
||||
profile: terminal.profile_id_opt.map(|profile_id| profile_id.0),
|
||||
working_directory: terminal.working_directory(),
|
||||
title: terminal.tab_title_override.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if tabs.is_empty() {
|
||||
tabs.push(LayoutTab::default());
|
||||
}
|
||||
LayoutNode::Pane { tabs }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Replace the window content with the given layout: rebuild the pane
|
||||
/// grid and spawn a terminal per saved tab.
|
||||
fn layout_apply(&mut self, layout: &LayoutNode) -> Task<Message> {
|
||||
self.pane_model.panes.restore();
|
||||
|
||||
// Rebuild the pane grid; leaf order matches `LayoutNode::leaves`
|
||||
self.pane_model.panes = pane_grid::State::with_configuration(
|
||||
layout.configuration(&mut || segmented_button::ModelBuilder::default().build()),
|
||||
);
|
||||
self.terminal_ids.clear();
|
||||
let panes: Vec<pane_grid::Pane> = self
|
||||
.pane_model
|
||||
.panes
|
||||
.iter()
|
||||
.map(|(pane, _)| *pane)
|
||||
.collect();
|
||||
for pane in &panes {
|
||||
self.terminal_ids.insert(*pane, widget::Id::unique());
|
||||
}
|
||||
self.pane_model.panes_created = panes.len();
|
||||
|
||||
let mut tasks = Vec::new();
|
||||
let default_tabs = vec![LayoutTab::default()];
|
||||
for (pane, tabs) in panes.iter().zip(layout.leaves()) {
|
||||
// Guard against hand-edited layouts with an empty tab list
|
||||
let tabs = if tabs.is_empty() { &default_tabs } else { tabs };
|
||||
for tab in tabs {
|
||||
let profile_id_opt = tab
|
||||
.profile
|
||||
.map(ProfileId)
|
||||
.filter(|profile_id| self.config.profiles.contains_key(profile_id));
|
||||
self.startup_options = Some(tty::Options {
|
||||
working_directory: tab.working_directory.clone(),
|
||||
..tty::Options::default()
|
||||
});
|
||||
tasks.push(self.create_and_focus_new_terminal(*pane, profile_id_opt, false));
|
||||
|
||||
// Restore custom tab titles
|
||||
if let Some(title) = &tab.title
|
||||
&& let Some(tab_model) = self.pane_model.panes.get_mut(*pane)
|
||||
{
|
||||
let entity = tab_model.active();
|
||||
tab_model.text_set(entity, title.clone());
|
||||
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
||||
terminal.lock().unwrap().tab_title_override = Some(title.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(first) = panes.first() {
|
||||
self.pane_model.set_focus(*first);
|
||||
}
|
||||
tasks.push(self.update_title(None));
|
||||
tasks.push(self.update_focus());
|
||||
Task::batch(tasks)
|
||||
}
|
||||
|
||||
fn create_and_focus_new_terminal(
|
||||
&mut self,
|
||||
pane: pane_grid::Pane,
|
||||
|
|
@ -2049,6 +2191,8 @@ impl Application for App {
|
|||
color_scheme_rename_id: widget::Id::unique(),
|
||||
rename_tab: None,
|
||||
rename_tab_id: widget::Id::unique(),
|
||||
layout_load_pending: None,
|
||||
ssh_hosts: ssh_config_hosts(),
|
||||
color_scheme_tab_model: widget::segmented_button::Model::default(),
|
||||
profile_expanded: None,
|
||||
show_advanced_font_settings: false,
|
||||
|
|
@ -2691,6 +2835,78 @@ impl Application for App {
|
|||
}
|
||||
}
|
||||
}
|
||||
Message::LayoutLoad => {
|
||||
if self.dialog_opt.is_none() {
|
||||
let (dialog, command) = Dialog::new(
|
||||
DialogSettings::new().kind(DialogKind::OpenFile),
|
||||
|msg| Message::DialogMessage(Box::new(msg)),
|
||||
Message::LayoutLoadResult,
|
||||
);
|
||||
self.dialog_opt = Some(dialog);
|
||||
return command;
|
||||
}
|
||||
}
|
||||
Message::LayoutLoadApply => {
|
||||
if let Some(layout) = self.layout_load_pending.take() {
|
||||
return self.layout_apply(&layout);
|
||||
}
|
||||
}
|
||||
Message::LayoutLoadCancel => {
|
||||
self.layout_load_pending = None;
|
||||
self.pane_model.update_terminal_focus();
|
||||
return self.update_focus();
|
||||
}
|
||||
Message::LayoutLoadResult(result) => {
|
||||
self.dialog_opt = None;
|
||||
if let DialogResult::Open(paths) = result {
|
||||
let path = &paths[0];
|
||||
match fs::read_to_string(path) {
|
||||
Ok(content) => match ron::from_str::<LayoutNode>(&content) {
|
||||
Ok(layout) => {
|
||||
// Ask for confirmation before replacing sessions
|
||||
self.layout_load_pending = Some(layout);
|
||||
self.pane_model.unfocus_all_terminals();
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to parse layout {:?}: {}", path, err);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("failed to read layout {:?}: {}", path, err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::LayoutSave => {
|
||||
if self.dialog_opt.is_none() {
|
||||
let (dialog, command) = Dialog::new(
|
||||
DialogSettings::new().kind(DialogKind::SaveFile {
|
||||
filename: String::from("cosmic-term-layout.ron"),
|
||||
}),
|
||||
|msg| Message::DialogMessage(Box::new(msg)),
|
||||
Message::LayoutSaveResult,
|
||||
);
|
||||
self.dialog_opt = Some(dialog);
|
||||
return command;
|
||||
}
|
||||
}
|
||||
Message::LayoutSaveResult(result) => {
|
||||
self.dialog_opt = None;
|
||||
if let DialogResult::Open(paths) = result {
|
||||
let path = &paths[0];
|
||||
let layout = self.layout_from_state(self.pane_model.panes.layout());
|
||||
match ron::ser::to_string_pretty(&layout, ron::ser::PrettyConfig::new()) {
|
||||
Ok(ron) => {
|
||||
if let Err(err) = fs::write(path, ron) {
|
||||
log::error!("failed to save layout to {:?}: {}", path, err);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to serialize layout: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Modifiers(modifiers) => {
|
||||
self.modifiers = modifiers;
|
||||
}
|
||||
|
|
@ -3085,6 +3301,26 @@ impl Application for App {
|
|||
return self.update_config();
|
||||
}
|
||||
}
|
||||
Message::SshConnect(index) => {
|
||||
if let Some(host) = self.ssh_hosts.get(index).cloned() {
|
||||
self.startup_options = Some(tty::Options {
|
||||
shell: Some(tty::Shell::new("ssh".to_string(), vec![host.clone()])),
|
||||
..tty::Options::default()
|
||||
});
|
||||
let task =
|
||||
self.create_and_focus_new_terminal(self.pane_model.focused(), None, false);
|
||||
|
||||
// Name the tab after the host
|
||||
if let Some(tab_model) = self.pane_model.active_mut() {
|
||||
let entity = tab_model.active();
|
||||
tab_model.text_set(entity, host.clone());
|
||||
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
||||
terminal.lock().unwrap().tab_title_override = Some(host);
|
||||
}
|
||||
}
|
||||
return Task::batch([task, self.update_title(None)]);
|
||||
}
|
||||
}
|
||||
Message::ShowAdvancedFontSettings(show) => {
|
||||
self.show_advanced_font_settings = show;
|
||||
}
|
||||
|
|
@ -3760,6 +3996,21 @@ impl Application for App {
|
|||
}
|
||||
|
||||
fn dialog(&self) -> Option<Element<'_, Message>> {
|
||||
if self.layout_load_pending.is_some() {
|
||||
return Some(
|
||||
widget::dialog()
|
||||
.title(fl!("load-layout-title"))
|
||||
.body(fl!("load-layout-body"))
|
||||
.primary_action(
|
||||
widget::button::destructive(fl!("load")).on_press(Message::LayoutLoadApply),
|
||||
)
|
||||
.secondary_action(
|
||||
widget::button::standard(fl!("cancel")).on_press(Message::LayoutLoadCancel),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some((_, buffer)) = self.rename_tab.as_ref() {
|
||||
let input = widget::text_input(fl!("tab-name-placeholder"), buffer)
|
||||
.id(self.rename_tab_id.clone())
|
||||
|
|
@ -3818,6 +4069,7 @@ impl Application for App {
|
|||
&self.config,
|
||||
&self.key_binds,
|
||||
self.active_terminal_monitors(),
|
||||
&self.ssh_hosts,
|
||||
)]
|
||||
}
|
||||
|
||||
|
|
|
|||
43
src/menu.rs
43
src/menu.rs
|
|
@ -219,6 +219,7 @@ pub fn menu_bar<'a>(
|
|||
config: &Config,
|
||||
key_binds: &HashMap<KeyBind, Action>,
|
||||
monitors: (bool, bool, bool),
|
||||
ssh_hosts: &[String],
|
||||
) -> Element<'a, Message> {
|
||||
let mut profile_items = Vec::with_capacity(config.profiles.len());
|
||||
for (name, id) in config.profile_names() {
|
||||
|
|
@ -227,6 +228,32 @@ pub fn menu_bar<'a>(
|
|||
|
||||
//TODO: what to do if there are no profiles?
|
||||
|
||||
let mut file_items = vec![
|
||||
MenuItem::Button(fl!("new-tab"), None, Action::TabNew),
|
||||
MenuItem::Button(fl!("new-window"), None, Action::WindowNew),
|
||||
MenuItem::Divider,
|
||||
MenuItem::Folder(fl!("profile"), profile_items),
|
||||
MenuItem::Button(fl!("menu-profiles"), None, Action::Profiles),
|
||||
MenuItem::Divider,
|
||||
];
|
||||
if !ssh_hosts.is_empty() {
|
||||
let ssh_items = ssh_hosts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, host)| MenuItem::Button(host.clone(), None, Action::SshConnect(index)))
|
||||
.collect();
|
||||
file_items.push(MenuItem::Folder(fl!("ssh"), ssh_items));
|
||||
}
|
||||
file_items.extend([
|
||||
MenuItem::Button(fl!("save-layout"), None, Action::LayoutSave),
|
||||
MenuItem::Button(fl!("load-layout"), None, Action::LayoutLoad),
|
||||
MenuItem::Divider,
|
||||
MenuItem::Button(fl!("rename-tab"), None, Action::TabRename),
|
||||
MenuItem::Button(fl!("close-tab"), None, Action::TabClose),
|
||||
MenuItem::Divider,
|
||||
MenuItem::Button(fl!("quit"), None, Action::WindowClose),
|
||||
]);
|
||||
|
||||
let color_scheme_kind = config.color_scheme_kind(core.system_theme());
|
||||
|
||||
responsive_menu_bar()
|
||||
|
|
@ -239,21 +266,7 @@ pub fn menu_bar<'a>(
|
|||
MENU_ID.clone(),
|
||||
Message::Surface,
|
||||
vec![
|
||||
(
|
||||
fl!("file"),
|
||||
vec![
|
||||
MenuItem::Button(fl!("new-tab"), None, Action::TabNew),
|
||||
MenuItem::Button(fl!("new-window"), None, Action::WindowNew),
|
||||
MenuItem::Divider,
|
||||
MenuItem::Folder(fl!("profile"), profile_items),
|
||||
MenuItem::Button(fl!("menu-profiles"), None, Action::Profiles),
|
||||
MenuItem::Divider,
|
||||
MenuItem::Button(fl!("rename-tab"), None, Action::TabRename),
|
||||
MenuItem::Button(fl!("close-tab"), None, Action::TabClose),
|
||||
MenuItem::Divider,
|
||||
MenuItem::Button(fl!("quit"), None, Action::WindowClose),
|
||||
],
|
||||
),
|
||||
(fl!("file"), file_items),
|
||||
(
|
||||
fl!("edit"),
|
||||
vec![
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue