feat: align terminal with local cosmic stack

This commit is contained in:
Lionel DARNIS 2026-05-24 10:27:32 +02:00
parent 1e49511fcf
commit f9f329d85f
7 changed files with 183 additions and 46 deletions

View file

@ -36,15 +36,17 @@ thiserror = { version = "2.0", optional = true }
secstr = { version = "0.5", optional = true } secstr = { version = "0.5", optional = true }
[dependencies.cosmic-files] [dependencies.cosmic-files]
git = "https://github.com/pop-os/cosmic-files.git" path = "../cosmic-files"
default-features = false default-features = false
[dependencies.cosmic-text] [dependencies.cosmic-text]
version = "0.19" git = "https://forge.aditua.com/leyoda/cosmic-text.git"
branch = "local/pr-503"
features = ["monospace_fallback", "shape-run-cache"] features = ["monospace_fallback", "shape-run-cache"]
[dependencies.libcosmic] [dependencies.cosmic]
git = "https://github.com/pop-os/libcosmic.git" package = "libcosmic-yoda"
path = "../libcosmic"
default-features = false default-features = false
#TODO: a11y feature crashes file chooser dialog #TODO: a11y feature crashes file chooser dialog
features = ["about", "autosize", "multi-window", "tokio", "winit", "surface-message"] features = ["about", "autosize", "multi-window", "tokio", "winit", "surface-message"]
@ -57,9 +59,9 @@ xdgen = "0.1"
[features] [features]
default = ["dbus-config", "wgpu", "wayland", "password_manager"] default = ["dbus-config", "wgpu", "wayland", "password_manager"]
dbus-config = ["libcosmic/dbus-config"] dbus-config = ["cosmic/dbus-config"]
wgpu = ["libcosmic/wgpu", "cosmic-files/wgpu"] wgpu = ["cosmic/wgpu", "cosmic-files/wgpu"]
wayland = ["libcosmic/wayland", "cosmic-files/wayland"] wayland = ["cosmic/wayland", "cosmic-files/wayland"]
password_manager = ["secret-service", "thiserror", "secstr"] password_manager = ["secret-service", "thiserror", "secstr"]
[profile.release-with-debug] [profile.release-with-debug]
@ -67,7 +69,29 @@ inherits = "release"
debug = true debug = true
#[patch.'https://github.com/pop-os/libcosmic'] [patch.'https://github.com/pop-os/libcosmic']
#libcosmic = { path = "../libcosmic" } cosmic-config = { path = "../libcosmic/cosmic-config" }
#cosmic-config = { path = "../libcosmic/cosmic-config" } cosmic-theme = { path = "../libcosmic/cosmic-theme" }
#cosmic-theme = { path = "../libcosmic/cosmic-theme" } iced = { path = "../libcosmic/iced" }
iced_accessibility = { path = "../libcosmic/iced/accessibility" }
iced_core = { path = "../libcosmic/iced/core" }
iced_futures = { path = "../libcosmic/iced/futures" }
iced_graphics = { path = "../libcosmic/iced/graphics" }
iced_renderer = { path = "../libcosmic/iced/renderer" }
iced_runtime = { path = "../libcosmic/iced/runtime" }
iced_tiny_skia = { path = "../libcosmic/iced/tiny_skia" }
iced_wgpu = { path = "../libcosmic/iced/wgpu" }
iced_widget = { path = "../libcosmic/iced/widget" }
iced_winit = { path = "../libcosmic/iced/winit" }
[patch.'https://github.com/pop-os/cosmic-protocols']
cosmic-protocols = { path = "../cosmic-protocols" }
cosmic-client-toolkit = { path = "../cosmic-protocols/client-toolkit" }
[patch.'https://github.com/pop-os/cosmic-text.git']
cosmic-text = { git = "https://forge.aditua.com/leyoda/cosmic-text.git", branch = "local/pr-503" }
[patch.'https://forge.aditua.com/leyoda/window_clipboard.git']
window_clipboard = { path = "../window_clipboard" }
dnd = { path = "../window_clipboard/dnd" }
mime = { path = "../window_clipboard/mime" }

View file

@ -70,6 +70,7 @@ tab-new-inherit-working-directory-description = Open new tabs and windows in the
add-another-keybinding = Add another keybinding add-another-keybinding = Add another keybinding
cancel = Cancel cancel = Cancel
close-window = Close window close-window = Close window
confirm = Confirm
copy-or-sigint = Copy or SIGINT copy-or-sigint = Copy or SIGINT
disable = Disable disable = Disable
focus-pane-down = Focus pane down focus-pane-down = Focus pane down
@ -83,6 +84,7 @@ password-manager = Password manager
paste-primary = Paste primary paste-primary = Paste primary
replace = Replace replace = Replace
reset-to-default = Reset to default reset-to-default = Reset to default
reset = Reset
shortcut-capture-hint = Press the key combination shortcut-capture-hint = Press the key combination
shortcut-group-clipboard = Clipboard shortcut-group-clipboard = Clipboard
shortcut-group-other = Other shortcut-group-other = Other
@ -92,6 +94,9 @@ shortcut-group-zoom = Zoom
shortcut-replace-body = { $binding } is already assigned to { $existing }. Replace it with { $new_action }? shortcut-replace-body = { $binding } is already assigned to { $existing }. Replace it with { $new_action }?
shortcut-replace-title = Replace shortcut? shortcut-replace-title = Replace shortcut?
tab-activate = Activate tab { $number } tab-activate = Activate tab { $number }
rename-tab = Rename tab
tab-name-placeholder = Tab name
tab-rename-title = Rename tab
toggle-fullscreen = Toggle fullscreen toggle-fullscreen = Toggle fullscreen
type-to-search = Type to search... type-to-search = Type to search...

View file

@ -121,6 +121,7 @@ copy-link = Copier le lien
add-another-keybinding = Ajouter un autre raccourci clavier add-another-keybinding = Ajouter un autre raccourci clavier
cancel = Annuler cancel = Annuler
close-window = Fermer la fenêtre close-window = Fermer la fenêtre
confirm = Confirmer
copy-or-sigint = Copier ou SIGINT copy-or-sigint = Copier ou SIGINT
disable = Désactiver disable = Désactiver
keyboard-shortcuts = Raccourcis clavier keyboard-shortcuts = Raccourcis clavier
@ -144,7 +145,11 @@ focus-pane-left = Passer au panneau de gauche
focus-pane-right = Passer au panneau de droite focus-pane-right = Passer au panneau de droite
focus-pane-up = Passer au panneau du dessus focus-pane-up = Passer au panneau du dessus
reset-to-default = Rétablir les paramètres par défaut reset-to-default = Rétablir les paramètres par défaut
reset = Réinitialiser
toggle-fullscreen = Basculer en plein écran toggle-fullscreen = Basculer en plein écran
rename-tab = Renommer l'onglet
tab-name-placeholder = Nom de l'onglet
tab-rename-title = Renommer l'onglet
comment = Émulateur de terminal pour le bureau COSMIC comment = Émulateur de terminal pour le bureau COSMIC
keywords = Commande;Shell;Terminal;CLI; keywords = Commande;Shell;Terminal;CLI;
tab-new-inherit-working-directory = Les nouveaux onglets et fenêtres utilisent le répertoire actuel tab-new-inherit-working-directory = Les nouveaux onglets et fenêtres utilisent le répertoire actuel

View file

@ -281,6 +281,7 @@ pub enum Action {
TabNewNoProfile, TabNewNoProfile,
TabNext, TabNext,
TabPrev, TabPrev,
TabRename,
ToggleFullscreen, ToggleFullscreen,
WindowClose, WindowClose,
WindowNew, WindowNew,
@ -334,6 +335,7 @@ impl Action {
Self::TabNewNoProfile => Message::TabNewNoProfile, Self::TabNewNoProfile => Message::TabNewNoProfile,
Self::TabNext => Message::TabNext, Self::TabNext => Message::TabNext,
Self::TabPrev => Message::TabPrev, Self::TabPrev => Message::TabPrev,
Self::TabRename => Message::TabRenameOpen(entity_opt),
Self::ToggleFullscreen => Message::ToggleFullscreen, Self::ToggleFullscreen => Message::ToggleFullscreen,
Self::WindowClose => Message::WindowClose, Self::WindowClose => Message::WindowClose,
Self::WindowNew => Message::WindowNew, Self::WindowNew => Message::WindowNew,
@ -441,6 +443,12 @@ pub enum Message {
TabNewNoProfile, TabNewNoProfile,
TabNext, TabNext,
TabPrev, TabPrev,
TabRenameOpenForPane(pane_grid::Pane, segmented_button::Entity),
TabRenameOpen(Option<segmented_button::Entity>),
TabRenameInput(String),
TabRenameCommit,
TabRenameCancel,
TabRenameResetToDefault,
TermEvent(pane_grid::Pane, segmented_button::Entity, TermEvent), TermEvent(pane_grid::Pane, segmented_button::Entity, TermEvent),
TermEventTx(mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, TermEvent)>), TermEventTx(mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, TermEvent)>),
ToggleFullscreen, ToggleFullscreen,
@ -514,6 +522,8 @@ pub struct App {
color_scheme_expanded: Option<(ColorSchemeKind, Option<ColorSchemeId>)>, color_scheme_expanded: Option<(ColorSchemeKind, Option<ColorSchemeId>)>,
color_scheme_renaming: Option<(ColorSchemeKind, ColorSchemeId, String)>, color_scheme_renaming: Option<(ColorSchemeKind, ColorSchemeId, String)>,
color_scheme_rename_id: widget::Id, color_scheme_rename_id: widget::Id,
rename_tab: Option<(segmented_button::Entity, String)>,
rename_tab_id: widget::Id,
color_scheme_tab_model: widget::segmented_button::SingleSelectModel, color_scheme_tab_model: widget::segmented_button::SingleSelectModel,
profile_expanded: Option<ProfileId>, profile_expanded: Option<ProfileId>,
show_advanced_font_settings: bool, show_advanced_font_settings: bool,
@ -764,7 +774,9 @@ impl App {
} }
fn update_focus(&self) -> Task<Message> { fn update_focus(&self) -> Task<Message> {
if self.find { if self.rename_tab.is_some() {
widget::text_input::focus(self.rename_tab_id.clone())
} else if self.find {
widget::text_input::focus(self.find_search_id.clone()) widget::text_input::focus(self.find_search_id.clone())
} else if self.core.window.show_context { } else if self.core.window.show_context {
// Right now we only care about the KeyboardShortcuts context page, so we use a simple if. // Right now we only care about the KeyboardShortcuts context page, so we use a simple if.
@ -1884,6 +1896,8 @@ impl Application for App {
color_scheme_expanded: None, color_scheme_expanded: None,
color_scheme_renaming: None, color_scheme_renaming: None,
color_scheme_rename_id: widget::Id::unique(), color_scheme_rename_id: widget::Id::unique(),
rename_tab: None,
rename_tab_id: widget::Id::unique(),
color_scheme_tab_model: widget::segmented_button::Model::default(), color_scheme_tab_model: widget::segmented_button::Model::default(),
profile_expanded: None, profile_expanded: None,
show_advanced_font_settings: false, show_advanced_font_settings: false,
@ -3031,6 +3045,64 @@ impl Application for App {
} }
} }
} }
Message::TabRenameOpenForPane(pane, entity) => {
self.pane_model.set_focus(pane);
return self.update(Message::TabRenameOpen(Some(entity)));
}
Message::TabRenameOpen(entity_opt) => {
if let Some(tab_model) = self.pane_model.active() {
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
let initial = tab_model
.text(entity)
.map(|s| s.to_string())
.unwrap_or_default();
self.rename_tab = Some((entity, initial));
self.pane_model.unfocus_all_terminals();
return self.update_focus();
}
}
Message::TabRenameInput(value) => {
if let Some((_, buffer)) = self.rename_tab.as_mut() {
*buffer = value;
}
}
Message::TabRenameCommit => {
if let Some((entity, value)) = self.rename_tab.take() {
let trimmed = value.trim().to_string();
if let Some(tab_model) = self.pane_model.active_mut() {
if trimmed.is_empty() {
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
terminal.lock().unwrap().tab_title_override = None;
}
tab_model.text_set(entity, fl!("new-terminal"));
} else {
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
terminal.lock().unwrap().tab_title_override = Some(trimmed.clone());
}
tab_model.text_set(entity, trimmed);
}
}
self.pane_model.update_terminal_focus();
return self.update_title(None);
}
}
Message::TabRenameCancel => {
self.rename_tab = None;
self.pane_model.update_terminal_focus();
}
Message::TabRenameResetToDefault => {
if let Some((entity, _)) = self.rename_tab.take()
&& let Some(tab_model) = self.pane_model.active_mut()
{
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
terminal.lock().unwrap().tab_title_override = None;
}
tab_model.text_set(entity, fl!("new-terminal"));
self.pane_model.update_terminal_focus();
return self.update_title(None);
}
self.pane_model.update_terminal_focus();
}
Message::TermEvent(pane, entity, event) => { Message::TermEvent(pane, entity, event) => {
match event { match event {
TermEvent::Bell => { TermEvent::Bell => {
@ -3356,6 +3428,31 @@ impl Application for App {
} }
fn dialog(&self) -> Option<Element<'_, Message>> { fn dialog(&self) -> Option<Element<'_, Message>> {
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())
.on_input(Message::TabRenameInput)
.on_submit(|_| Message::TabRenameCommit);
return Some(
widget::dialog()
.title(fl!("tab-rename-title"))
.control(input)
.primary_action(
widget::button::suggested(fl!("confirm"))
.on_press(Message::TabRenameCommit),
)
.secondary_action(
widget::button::standard(fl!("cancel")).on_press(Message::TabRenameCancel),
)
.tertiary_action(
widget::button::text(fl!("reset"))
.on_press(Message::TabRenameResetToDefault),
)
.into(),
);
}
let conflict = self.shortcut_conflict.as_ref()?; let conflict = self.shortcut_conflict.as_ref()?;
let binding = shortcuts::binding_display(&conflict.binding); let binding = shortcuts::binding_display(&conflict.binding);
let existing = shortcuts::action_label(conflict.existing_action); let existing = shortcuts::action_label(conflict.existing_action);
@ -3430,34 +3527,31 @@ impl Application for App {
let pane_grid = PaneGrid::new(&self.pane_model.panes, |pane, tab_model, _is_maximized| { let pane_grid = PaneGrid::new(&self.pane_model.panes, |pane, tab_model, _is_maximized| {
let mut tab_column = widget::column::with_capacity(1); let mut tab_column = widget::column::with_capacity(1);
if tab_model.iter().count() > 1 { tab_column = tab_column.push(
tab_column = tab_column.push( widget::container(
widget::container( widget::tab_bar::horizontal(tab_model)
widget::tab_bar::horizontal(tab_model) .enable_tab_drag(String::from("x-cosmic-term/tab"))
.enable_tab_drag(String::from("x-cosmic-term/tab")) .on_reorder(move |event| Message::ReorderTab(pane, event))
.on_reorder(move |event| Message::ReorderTab(pane, event)) .tab_drag_threshold(25.)
.tab_drag_threshold(25.) .button_height(32)
.button_height(32) .button_spacing(space_xxs)
.button_spacing(space_xxs) .on_activate(Message::TabActivate)
.on_activate(Message::TabActivate) .on_close(|entity| Message::TabClose(Some(entity)))
.on_close(|entity| Message::TabClose(Some(entity))), .on_context(move |entity| Message::TabRenameOpenForPane(pane, entity)),
) )
.class(style::Container::Custom(Box::new(|theme| { .class(style::Container::Custom(Box::new(|theme| {
let cosmic = theme.cosmic(); let cosmic = theme.cosmic();
cosmic::iced::widget::container::Style { cosmic::iced::widget::container::Style {
icon_color: Some(Color::from(cosmic.background.on)), icon_color: Some(Color::from(cosmic.background.on)),
text_color: Some(Color::from(cosmic.background.on)), text_color: Some(Color::from(cosmic.background.on)),
background: Some(iced::Background::Color( background: Some(iced::Background::Color(cosmic.background.base.into())),
cosmic.background.base.into(), border: iced::Border::default(),
)), shadow: iced::Shadow::default(),
border: iced::Border::default(), snap: true,
shadow: iced::Shadow::default(), }
snap: true, })))
} .width(Length::Fill),
}))) );
.width(Length::Fill),
);
}
let entity = tab_model.active(); let entity = tab_model.active();
let entity_middle_click = tab_model.active(); let entity_middle_click = tab_model.active();
@ -3469,7 +3563,7 @@ impl Application for App {
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) { if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal_box = terminal_box(terminal, &self.key_binds) let mut terminal_box = terminal_box(terminal, &self.key_binds)
.id(terminal_id) .id(terminal_id)
.disabled(self.core.window.show_context) .disabled(self.core.window.show_context || self.rename_tab.is_some())
.on_context_menu(move |menu_state| Message::TabContextMenu(pane, menu_state)) .on_context_menu(move |menu_state| Message::TabContextMenu(pane, menu_state))
.on_middle_click(move || Message::MiddleClick(pane, Some(entity_middle_click))) .on_middle_click(move || Message::MiddleClick(pane, Some(entity_middle_click)))
.on_open_hyperlink(Some(Box::new(Message::LaunchUrl))) .on_open_hyperlink(Some(Box::new(Message::LaunchUrl)))

View file

@ -95,7 +95,9 @@ pub fn context_menu<'a>(
)), )),
Element::from(divider::horizontal::light()), Element::from(divider::horizontal::light()),
Element::from(menu_item(fl!("new-tab"), Action::TabNew)), Element::from(menu_item(fl!("new-tab"), Action::TabNew)),
Element::from(menu_item(fl!("rename-tab"), Action::TabRename)),
Element::from(menu_item(fl!("menu-settings"), Action::Settings)), Element::from(menu_item(fl!("menu-settings"), Action::Settings)),
Element::from(menu_item(fl!("menu-about"), Action::About)),
]; ];
#[cfg(feature = "password_manager")] #[cfg(feature = "password_manager")]
{ {
@ -227,6 +229,7 @@ pub fn menu_bar<'a>(
MenuItem::Folder(fl!("profile"), profile_items), MenuItem::Folder(fl!("profile"), profile_items),
MenuItem::Button(fl!("menu-profiles"), None, Action::Profiles), MenuItem::Button(fl!("menu-profiles"), None, Action::Profiles),
MenuItem::Divider, MenuItem::Divider,
MenuItem::Button(fl!("rename-tab"), None, Action::TabRename),
MenuItem::Button(fl!("close-tab"), None, Action::TabClose), MenuItem::Button(fl!("close-tab"), None, Action::TabClose),
MenuItem::Divider, MenuItem::Divider,
MenuItem::Button(fl!("quit"), None, Action::WindowClose), MenuItem::Button(fl!("quit"), None, Action::WindowClose),

View file

@ -87,6 +87,7 @@ pub enum KeyBindAction {
TabNew, TabNew,
TabNext, TabNext,
TabPrev, TabPrev,
TabRename,
ToggleFullscreen, ToggleFullscreen,
WindowClose, WindowClose,
WindowNew, WindowNew,
@ -127,6 +128,7 @@ impl KeyBindAction {
Self::TabNew => Some(Action::TabNew), Self::TabNew => Some(Action::TabNew),
Self::TabNext => Some(Action::TabNext), Self::TabNext => Some(Action::TabNext),
Self::TabPrev => Some(Action::TabPrev), Self::TabPrev => Some(Action::TabPrev),
Self::TabRename => Some(Action::TabRename),
Self::ToggleFullscreen => Some(Action::ToggleFullscreen), Self::ToggleFullscreen => Some(Action::ToggleFullscreen),
Self::WindowClose => Some(Action::WindowClose), Self::WindowClose => Some(Action::WindowClose),
Self::WindowNew => Some(Action::WindowNew), Self::WindowNew => Some(Action::WindowNew),
@ -288,6 +290,7 @@ pub fn action_label(action: KeyBindAction) -> String {
KeyBindAction::TabNew => fl!("new-tab"), KeyBindAction::TabNew => fl!("new-tab"),
KeyBindAction::TabNext => fl!("next-tab"), KeyBindAction::TabNext => fl!("next-tab"),
KeyBindAction::TabPrev => fl!("previous-tab"), KeyBindAction::TabPrev => fl!("previous-tab"),
KeyBindAction::TabRename => fl!("rename-tab"),
KeyBindAction::ToggleFullscreen => fl!("toggle-fullscreen"), KeyBindAction::ToggleFullscreen => fl!("toggle-fullscreen"),
KeyBindAction::WindowClose => fl!("close-window"), KeyBindAction::WindowClose => fl!("close-window"),
KeyBindAction::WindowNew => fl!("new-window"), KeyBindAction::WindowNew => fl!("new-window"),
@ -320,6 +323,7 @@ pub fn shortcut_groups() -> Vec<ShortcutGroup> {
actions: vec![ actions: vec![
KeyBindAction::TabNew, KeyBindAction::TabNew,
KeyBindAction::TabClose, KeyBindAction::TabClose,
KeyBindAction::TabRename,
KeyBindAction::TabNext, KeyBindAction::TabNext,
KeyBindAction::TabPrev, KeyBindAction::TabPrev,
KeyBindAction::TabActivate0, KeyBindAction::TabActivate0,
@ -454,6 +458,7 @@ fn fallback_shortcuts() -> Shortcuts {
bind!([Ctrl, Shift], "V", Paste); bind!([Ctrl, Shift], "V", Paste);
bind!([Shift], "Insert", PastePrimary); bind!([Shift], "Insert", PastePrimary);
bind!([Ctrl, Shift], "W", TabClose); bind!([Ctrl, Shift], "W", TabClose);
bind!([Ctrl, Shift], "R", TabRename);
bind!([Ctrl], ",", Settings); bind!([Ctrl], ",", Settings);
bind!([], "F11", ToggleFullscreen); bind!([], "F11", ToggleFullscreen);

View file

@ -197,11 +197,12 @@ impl TerminalPaneGrid {
} }
pub fn unfocus_all_terminals(&self) { pub fn unfocus_all_terminals(&self) {
for (_pane, tab_model) in self.panes.panes.iter() { for (_pane, tab_model) in self.panes.panes.iter() {
let entity = tab_model.active(); for entity in tab_model.iter() {
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) { if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap(); let mut terminal = terminal.lock().unwrap();
terminal.set_focused(false); terminal.set_focused(false);
terminal.update(); terminal.update();
}
} }
} }
} }