Compare commits

..

14 commits

Author SHA1 Message Date
81dc7d3eeb chore: fige Cargo.lock (epoch-1.9.0, [patch] locaux)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 21:03:54 +02:00
083e09f97e merge: epoch-1.9.0 d'upstream
# Conflicts:
#	Cargo.lock
#	src/main.rs
#	src/menu.rs
2026-09-24 20:52:15 +02:00
Hojjat
9129277bed chore: update libcosmic to fix esc handling in menus 2026-09-23 11:55:55 -06:00
Hojjat
78eaba9db1 chore: tweak context menu items order and wording 2026-09-15 19:09:43 -04:00
Michael Aaron Murphy
d25c2108c0 chore: update cosmic-files 2026-09-15 19:09:43 -04:00
Hojjat
120b4fdb12 feat: use libcosmic's context_menu widget for the terminal context menu 2026-09-15 19:09:43 -04:00
Michael Aaron Murphy
4cd8f8d605 Epoch 1.9.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-09-15 19:09:43 -04:00
Michael Murphy
1d9e5784d2
i18n: translation update from Hosted Weblate (#908)
Translations update from [Hosted Weblate](https://hosted.weblate.org)
for [Pop OS/COSMIC
Term](https://hosted.weblate.org/projects/pop-os/cosmic-term/).



Current translation status:

![Weblate translation
status](https://hosted.weblate.org/widget/pop-os/cosmic-term/matrix-auto.svg)
2026-09-15 21:57:44 +02:00
Hosted Weblate
327febcc92
i18n: translation updates from weblate
Co-authored-by: Amadɣas <massiin@proton.me>
Co-authored-by: Blauen <butterflee555@gmail.com>
Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com>
Co-authored-by: Geeson Wan <wang14240@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Kurt Harren Catindig <kurtharrencatindig03@gmail.com>
Co-authored-by: Quentin PAGÈS <quentinantonin@free.fr>
Co-authored-by: Satakun Utama <galaxytomostore@gmail.com>
Co-authored-by: VandaL <vandalhj@gmail.com>
Co-authored-by: Zahid Rizky Fakhri <zahidrizkyfakhri@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/id/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/kab/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/kmr/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/oc/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/pl/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/th/
Translate-URL: https://hosted.weblate.org/projects/pop-os/cosmic-term/zh_Hant/
Translation: Pop OS/COSMIC Term
2026-09-13 01:56:16 +02:00
99842bde03 chore: fige Cargo.lock 2026-09-10 18:12:37 +02:00
5fc081e257 Merge tag 'epoch-1.8.0' into local/yoda-main
Epoch 1.8.0

# Conflicts:
#	Cargo.lock
2026-09-10 17:57:28 +02:00
b8fce5a272 yoda: elements de bloc U+2580-259F peints en quads natifs
Le glyphe de police d'un bloc ne couvre que ~1 em alors que la cellule
fait line_height (~1.4 em) : rangées de blocs trouees par l'interligne,
bords creneles, rendu flou aux echelles fractionnees.

Desormais le caractere est remplace par une espace (aucun glyphe) et le
remplissage voyage dans Metadata jusqu'au tracé, ou terminal_box le
peint en quad GPU couvrant exactement la fraction de cellule (huitiemes,
quadrants, trames alpha). Consecutive cells de meme metadata fusionnent
en un seul quad : jointif par construction, aligne sur la grille, net a
toute echelle. C'est l'approche de kitty/WezTerm. Tout TUI a blocs
profite (bannieres, jauges htop/btop, barres de progression).

Couverture totale de la plage + geometrie bornee, 2 tests.
2026-09-04 16:53:35 +02:00
Levi Portenier
4f0f0b6e4e
Epoch 1.8.0 version update (#911)
Generated by cosmic-epoch scripts/version-update.sh
2026-09-02 12:25:35 -06:00
Jeremy Soller
7945b9ffc6 Epoch 1.8.0 version update
Generated by cosmic-epoch scripts/version-update.sh
2026-09-02 08:16:02 -06:00
17 changed files with 633 additions and 720 deletions

660
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "cosmic-term"
version = "1.7.0"
version = "1.9.0"
authors = ["Jeremy Soller <jeremy@system76.com>"]
edition = "2024"
license = "GPL-3.0-only"

12
debian/changelog vendored
View file

@ -1,3 +1,15 @@
cosmic-term (1.9.0) noble; urgency=medium
* Epoch 1.9.0 version update
-- Michael Murphy <michael@mmurphy.dev> Mon, 14 Sep 2026 19:50:47 +0200
cosmic-term (1.8.0) noble; urgency=medium
* Epoch 1.8.0 version update
-- Jeremy Soller <jeremy@system76.com> Wed, 02 Sep 2026 08:15:55 -0600
cosmic-term (1.7.0) noble; urgency=medium
* Epoch 1.7.0 version update

View file

@ -1,4 +1,6 @@
tar-ignore=.github
tar-ignore=.vscode
tar-ignore=target/.rustc_info.json
tar-ignore=target/CACHEDIR.TAG
tar-ignore=target/debug
tar-ignore=target/release
tar-ignore=vendor
tar-ignore=target

View file

@ -63,6 +63,7 @@ show-pane-borders = Show pane borders
### Advanced
advanced = Advanced
show-headerbar = Show header
window-header = Window header
show-header-description = Reveal the header from the right-click menu
tab-new-inherit-working-directory = New tabs and windows use current directory
tab-new-inherit-working-directory-description = Open new tabs and windows in the active tab's working directory
@ -152,7 +153,7 @@ next-tab = Next tab
previous-tab = Previous tab
split-horizontal = Split horizontal
split-vertical = Split vertical
pane-toggle-maximize = Toggle maximized
pane-toggle-maximize = Maximize or restore pane
menu-color-schemes = Color schemes...
menu-settings = Settings...
menu-about = About COSMIC Terminal...

0
i18n/fil/cosmic_term.ftl Normal file
View file

View file

@ -4,7 +4,7 @@ repository = Repositori
support = Dukungan
color-schemes = Skema warna
cosmic-terminal = Terminal COSMIC
rename = Ganti nama
rename = Ganti Nama
export = Ekspor
delete = Hapus
import = Impor

View file

@ -1,9 +1,9 @@
cancel = Sefsex
cancel = Semmet
name = Isem
delete = kkes
rename = Snifel isem
quit = Tuffɣa
support = Tallalt
quit = Ffeɣ
support = Tallelt
repository = Asarsay
settings = Iɣewwaṛen
appearance = Timeẓri
@ -14,7 +14,7 @@ paste = Senteḍ
select-all = Fren akk
view = Wali
menu-settings = Iɣewwaṛen…
match-desktop = Amṣada d tnarit
match-desktop = Semṣada akked tnarit
file = Afaylu
new-window = Asfaylu amaynut
edit = Ẓreg

View file

@ -27,7 +27,7 @@ light = Ronî
advanced = Pêşketî
support = Piştgirî
default-font = Cûrenivîs
paste = Pêve bike
paste = Pê ve bike
menu-settings = Sazkarî...
command-line = Rêza fermanê
view = Dîtin
@ -100,7 +100,7 @@ focus-pane-left = Balê bikişîne ser destgeha çepê
focus-pane-right = Balê bikişîne ser destgeha rastê
focus-pane-up = Balê bikişîne ser destgeha jorîn
password-manager = Rêveberê borînpeyvê
paste-primary = A sereke pêve bike
paste-primary = A sereke pê ve bike
reset-to-default = Vegere rewşa berdest
shortcut-capture-hint = Kombînasyona kilîtan bitikîne
shortcut-replace-body = { $binding } berê ji bo { $existing } hatiye destnîşankirin. Tu dixwazî bi { $new_action } re biguherînî?

View file

@ -1,7 +1,14 @@
settings = Paramètres
settings = Configuracion
appearance = Aparéncia
theme = Tèma
match-desktop = Assortir al burèu
dark = Fosc
dark = Escur
light = Clar
file = Fichièr
name = Nom
repository = Depaus
cancel = Anullar
menu-settings = Configuracion...
quit = Quitar
delete = Suprimir
replace = Remplaçar

View file

@ -1,5 +1,5 @@
cosmic-terminal = Terminal COSMIC
comment = Emulator terminala dla pulpitu COSMIC
comment = Emulator terminala dla środowiska COSMIC
keywords = Cli;WierszPoleceń;Powłoka;Terminal;
new-terminal = Nowy terminal
@ -73,7 +73,7 @@ advanced = Zaawansowane
show-headerbar = Pokaż pasek nagłówka
show-header-description = Ukazuje pasek nagłówka z menu wybieranego prawym przyciskiem myszki
# Find
find-placeholder = Szukaj…
find-placeholder = Wyszukaj…
find-previous = Znajdź poprzedni
find-next = Znajdź następny
@ -96,7 +96,7 @@ edit = Edytuj
copy = Kopiuj
paste = Wklej
select-all = Zaznacz wszystko
find = Szukaj
find = Wyszukaj
clear-scrollback = Wyczyść przewijanie
## View

View file

@ -39,8 +39,8 @@ settings = การตั้งค่า
appearance = ลักษณะ
theme = ธีม
match-desktop = ใช้ตามธีมหน้าจอหลัก
dark = ธีมมืด
light = ธีมสว่าง
dark = มืด
light = สว่าง
syntax-dark = โทนสีมืด
syntax-light = โทนสีสว่าง
default-zoom-step = ระยะซูม
@ -109,3 +109,8 @@ pane-toggle-maximize = ขยายให้ใหญ่สุด
menu-color-schemes = โทนสี...
menu-settings = การตั้งค่า...
menu-about = เกี่ยวกับ เทอร์มินัล COSMIC...
cancel = ยกเลิก
reset-to-default = รีเซ็ตเป็นค่าเริ่มต้น
type-to-search = พิมพ์เพื่อค้นหา
add-another-keybinding = เพิ่มคีย์ลัดเพิ่มเติม
replace = แทนที่

View file

@ -10,7 +10,7 @@ new-terminal = 新建終端機
## Color schemes
color-schemes = 配色方案
rename = 重命名
rename = 重新命名
export = 匯出
delete = 刪除
import = 匯入

View file

@ -566,7 +566,7 @@ pub enum Message {
ReorderTab(Pane, ReorderEvent),
SaveOutput(Option<segmented_button::Entity>),
SaveOutputResult(pane_grid::Pane, segmented_button::Entity, DialogResult),
Surface(surface::Action),
Surface(surface::Action<Message>),
SelectAll(Option<segmented_button::Entity>),
ShowAdvancedFontSettings(bool),
ShowHeaderBar(bool),
@ -604,7 +604,6 @@ pub enum Message {
ZoomIn,
ZoomOut,
ZoomReset,
ContextMenuPopupClosed(window::Id),
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
@ -682,14 +681,6 @@ pub struct App {
shortcut_search_regex: Option<regex::Regex>,
shortcut_search_value: String,
modifiers: Modifiers,
context_menu_popup: Option<(
window::Id,
pane_grid::Pane,
segmented_button::Entity,
Option<String>,
widget::Id,
cosmic::iced::Point,
)>,
#[cfg(feature = "password_manager")]
password_mgr: password_manager::PasswordManager,
}
@ -2213,7 +2204,6 @@ impl Application for App {
shortcut_search_regex: None,
shortcut_search_value: String::new(),
modifiers: Modifiers::empty(),
context_menu_popup: None,
#[cfg(feature = "password_manager")]
password_mgr: Default::default(),
};
@ -3433,16 +3423,7 @@ impl Application for App {
return self.update_title(None);
}
Message::TabContextAction(entity, action) => {
// Close context menu popup
let mut tasks = Vec::new();
if let Some((_popup_id, _, _, _, _, _)) = self.context_menu_popup.take() {
#[cfg(feature = "wayland")]
if is_wayland() {
tasks.push(cosmic::task::message(Message::Surface(
cosmic::surface::action::destroy_popup(_popup_id),
)));
}
}
// Close terminal context menu state
if let Some(tab_model) = self.pane_model.active()
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
@ -3451,11 +3432,8 @@ impl Application for App {
//Some actions need the menu_state,
//so only clear the position for them.
match action {
Action::LaunchUrlByMenu | Action::CopyUrlByMenu => {
if let Some(context_menu) = terminal.context_menu.as_mut() {
context_menu.position = None;
}
}
// these read the link from the menu state and clear it themselves
Action::LaunchUrlByMenu | Action::CopyUrlByMenu => {}
_ => {
terminal.context_menu = None;
}
@ -3465,94 +3443,29 @@ impl Application for App {
return cosmic::Task::batch(tasks);
}
Message::TabContextMenu(pane, menu_state) => {
#[allow(unused_mut)]
let mut tasks = Vec::new();
// Close existing context menu popup if any
if let Some((_popup_id, _, _, _, _, _)) = self.context_menu_popup.take() {
#[cfg(feature = "wayland")]
if is_wayland() {
tasks.push(cosmic::task::message(Message::Surface(
cosmic::surface::action::destroy_popup(_popup_id),
)));
}
}
// Clear all terminal context_menu state
for (_, tab_model) in self.pane_model.panes.iter() {
for entity in tab_model.iter() {
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.context_menu = None;
if terminal.context_menu.take().is_some() {
terminal.active_regex_match = None;
terminal.needs_update = true;
}
}
}
}
if let Some(menu_state) = menu_state {
if let Some(_position) = menu_state.position {
let local_position = menu_state.local_position.unwrap_or(_position);
if let Some(tab_model) = self.pane_model.panes.get(pane) {
let entity = tab_model.active();
let link = menu_state.link.clone();
let popup_id = window::Id::unique();
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
let mut terminal = terminal.lock().unwrap();
terminal.context_menu = Some(menu_state);
}
self.context_menu_popup = Some((
popup_id,
pane,
entity,
link,
widget::Id::unique(),
local_position,
));
#[cfg(feature = "wayland")]
if is_wayland() {
let main_window = self.core.main_window_id().unwrap();
let pos_x = _position.x as i32;
let pos_y = _position.y as i32;
tasks.push(cosmic::task::message(Message::Surface(
cosmic::surface::action::app_popup(
|_| Default::default(),
move |_app: &mut Self| {
use cosmic::cctk::wayland_protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity};
use cosmic::iced::runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner};
SctkPopupSettings {
parent: main_window,
id: popup_id,
positioner: SctkPositioner {
size: None,
anchor_rect: cosmic::iced::Rectangle {
x: pos_x,
y: pos_y,
width: 1,
height: 1,
},
anchor: Anchor::None,
gravity: Gravity::BottomRight,
reactive: true,
..Default::default()
},
parent_size: None,
grab: true,
close_with_children: false,
input_zone: None,
}
}, None),
)));
}
}
// A right press records what is under the cursor
if let Some(menu_state) = menu_state
&& let Some(tab_model) = self.pane_model.panes.get(pane)
{
let entity = tab_model.active();
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
terminal.lock().unwrap().context_menu = Some(menu_state);
}
self.pane_model.set_focus(pane);
}
return cosmic::Task::batch(tasks);
}
Message::TabNew => {
// The profile from `--profile` only applies to the first tab
@ -3944,26 +3857,8 @@ impl Application for App {
self.reset_active_pane_zoom();
return self.update_config();
}
Message::ContextMenuPopupClosed(id) => {
if let Some((popup_id, pane, entity, _, _, _)) = &self.context_menu_popup
&& id == *popup_id
{
// Clear link underline on the terminal
if let Some(tab_model) = self.pane_model.panes.get(*pane)
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(*entity)
{
let mut terminal = terminal.lock().unwrap();
terminal.context_menu = None;
terminal.active_regex_match = None;
terminal.needs_update = true;
}
self.context_menu_popup = None;
}
}
Message::Surface(a) => {
return cosmic::task::message(cosmic::Action::Cosmic(
cosmic::app::Action::Surface(a),
));
return cosmic::task::message(cosmic::Action::Surface(a));
}
Message::ReorderTab(
pane,
@ -4112,32 +4007,7 @@ impl Application for App {
]
}
fn on_close_requested(&self, id: window::Id) -> Option<Self::Message> {
if let Some((popup_id, _, _, _, _, _)) = &self.context_menu_popup
&& id == *popup_id
{
return Some(Message::ContextMenuPopupClosed(id));
}
None
}
fn view_window(&self, window_id: window::Id) -> Element<'_, Message> {
if let Some((popup_id, pane, entity, ref link, ref autosize_id, _)) =
self.context_menu_popup
&& window_id == popup_id
{
return widget::autosize::autosize(
menu::context_menu(
&self.config,
&self.key_binds,
entity,
link.clone(),
self.terminal_monitors(pane, entity),
),
autosize_id.clone(),
)
.into();
}
match &self.dialog_opt {
Some(dialog) => dialog.view(window_id),
None => widget::text("Unknown window ID").into(),
@ -4226,51 +4096,36 @@ impl Application for App {
terminal_box = terminal_box.on_mouse_enter(move || Message::MouseEnter(pane));
}
// If a context menu popup is active for this pane, inform the
// terminal_box so it will emit on_context_menu(None) on click
// to dismiss the popup.
if self.context_menu_popup.is_some() {
terminal_box = terminal_box.context_menu(cosmic::iced::Point::ORIGIN);
// The terminal records what was under the right press; the widget opens the
// menu on release, so build it from that state
let context_link = terminal
.lock()
.unwrap()
.context_menu
.as_ref()
.map(|menu_state| menu_state.link.clone());
if context_link.is_some() {
terminal_box = terminal_box.context_menu_open(true);
}
let use_wayland_popup = {
#[cfg(feature = "wayland")]
{
is_wayland()
}
#[cfg(not(feature = "wayland"))]
{
false
}
};
let tab_element: Element<'_, Message> = if !use_wayland_popup {
// Fallback: render context menu as an inline popover
if let Some((_, popup_pane, popup_entity, ref link, _, point)) =
self.context_menu_popup
{
if pane == popup_pane {
let mut popover = widget::popover(terminal_box.context_menu(point));
popover = popover
.popup(menu::context_menu(
&self.config,
&self.key_binds,
popup_entity,
link.clone(),
self.terminal_monitors(popup_pane, popup_entity),
))
.position(widget::popover::Position::Point(point));
popover.into()
} else {
terminal_box.into()
}
} else {
terminal_box.into()
}
} else {
terminal_box.into()
};
tab_column = tab_column.push(tab_element);
let mut context_menu = widget::context_menu(
terminal_box,
context_link.map(|link| {
menu::context_menu(
&self.config,
&self.key_binds,
entity,
link,
self.terminal_monitors(pane, entity),
)
}),
)
.item_width(widget::menu::ItemWidth::Uniform(360))
.on_close(Message::TabContextMenu(pane, None))
.on_surface_action(Message::Surface);
if let Some(window_id) = self.core.main_window_id() {
context_menu = context_menu.window_id(window_id);
}
tab_column = tab_column.push(context_menu);
}
//Only draw find in the currently focused pane
@ -4451,14 +4306,6 @@ impl Application for App {
}
}
#[cfg(feature = "wayland")]
fn is_wayland() -> bool {
matches!(
cosmic::app::cosmic::windowing_system(),
Some(cosmic::app::cosmic::WindowingSystem::Wayland)
)
}
/// Divider color painted behind the pane grid to form pane borders.
///
/// When blur is active the transparent container must be used, otherwise the

View file

@ -1,17 +1,15 @@
// SPDX-License-Identifier: GPL-3.0-only
use cosmic::iced::Point;
use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::{Item as MenuItem, menu_button};
use cosmic::widget::space;
use cosmic::{
Element,
app::Core,
iced::core::Border,
iced::{Background, Length, advanced::widget::text::Style as TextStyle},
theme,
iced::{Background, Length},
widget::{
self, divider,
self,
menu::{ItemHeight, ItemWidth},
responsive_menu_bar, segmented_button,
},
@ -23,147 +21,92 @@ use crate::{Action, ColorSchemeId, ColorSchemeKind, Config, Message, fl, termina
static MENU_ID: LazyLock<cosmic::widget::Id> =
LazyLock::new(|| cosmic::widget::Id::new("responsive-menu"));
/// What the terminal found under the right-click
#[derive(Debug, Clone)]
pub struct MenuState {
pub position: Option<Point>,
pub local_position: Option<Point>,
pub link: Option<String>,
}
pub fn context_menu<'a>(
/// A context menu action dispatched to the tab the menu was opened on.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TabAction(pub segmented_button::Entity, pub Action);
impl MenuAction for TabAction {
type Message = Message;
fn message(&self) -> Message {
Message::TabContextAction(self.0, self.1)
}
}
pub fn context_menu(
config: &Config,
key_binds: &HashMap<KeyBind, Action>,
entity: segmented_button::Entity,
link: Option<String>,
monitors: (bool, bool, bool),
) -> Element<'a, Message> {
let find_key = |action: &Action| -> String {
for (key_bind, key_action) in key_binds {
if action == key_action {
return key_bind.to_string();
}
}
String::new()
};
fn key_style(theme: &cosmic::Theme) -> TextStyle {
let mut color = theme.cosmic().background(theme.transparent).component.on;
color.alpha *= 0.75;
TextStyle {
color: Some(color.into()),
..Default::default()
}
}
let menu_item = |label, action| {
let key = find_key(&action);
menu_button(vec![
widget::text(label).into(),
space::horizontal().into(),
widget::text(key)
.class(theme::Text::Custom(key_style))
.into(),
])
.on_press(Message::TabContextAction(entity, action))
) -> Vec<widget::menu::Tree<Message>> {
let item =
|label: String, action: Action| MenuItem::Button(label, None, TabAction(entity, action));
let check = |label: String, value: bool, action: Action| {
MenuItem::CheckBox(label, None, value, TabAction(entity, action))
};
let menu_checkbox = |label, value, action| {
menu_button(vec![
widget::text(label).into(),
widget::space::horizontal().into(),
widget::toggler(value)
.on_toggle(move |_| Message::TabContextAction(entity, action))
.size(16.0)
.into(),
])
.on_press(Message::TabContextAction(entity, action))
};
let mut rows = vec![
Element::from(menu_item(fl!("copy"), Action::Copy)),
Element::from(menu_item(fl!("paste"), Action::Paste)),
Element::from(menu_item(fl!("select-all"), Action::SelectAll)),
Element::from(divider::horizontal::light()),
Element::from(menu_item(fl!("clear-scrollback"), Action::ClearScrollback)),
Element::from(menu_item(fl!("save-output"), Action::SaveOutput)),
Element::from(divider::horizontal::light()),
Element::from(menu_checkbox(
let mut items = vec![
item(fl!("new-tab"), Action::TabNew),
item(fl!("rename-tab"), Action::TabRename),
MenuItem::Divider,
item(fl!("copy"), Action::Copy),
item(fl!("paste"), Action::Paste),
item(fl!("select-all"), Action::SelectAll),
MenuItem::Divider,
item(fl!("clear-scrollback"), Action::ClearScrollback),
item(fl!("save-output"), Action::SaveOutput),
MenuItem::Divider,
check(
fl!("monitor-activity"),
monitors.0,
Action::Monitor(MonitorKind::Activity),
)),
Element::from(menu_checkbox(
),
check(
fl!("monitor-silence"),
monitors.1,
Action::Monitor(MonitorKind::Silence),
)),
Element::from(menu_checkbox(
),
check(
fl!("monitor-process-finished"),
monitors.2,
Action::Monitor(MonitorKind::ProcessFinished),
)),
Element::from(divider::horizontal::light()),
Element::from(menu_item(
fl!("split-horizontal"),
Action::PaneSplitHorizontal,
)),
Element::from(menu_item(fl!("split-vertical"), Action::PaneSplitVertical)),
Element::from(menu_item(
fl!("pane-toggle-maximize"),
Action::PaneToggleMaximized,
)),
Element::from(divider::horizontal::light()),
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-about"), Action::About)),
),
MenuItem::Divider,
item(fl!("split-horizontal"), Action::PaneSplitHorizontal),
item(fl!("split-vertical"), Action::PaneSplitVertical),
item(fl!("pane-toggle-maximize"), Action::PaneToggleMaximized),
MenuItem::Divider,
MenuItem::CheckBox(
fl!("window-header"),
None,
config.show_headerbar,
TabAction(entity, Action::ShowHeaderBar(!config.show_headerbar)),
),
MenuItem::Divider,
];
#[cfg(feature = "password_manager")]
{
rows.push(Element::from(menu_item(
fl!("menu-password-manager"),
Action::PasswordManager,
)));
}
rows.push(Element::from(menu_checkbox(
fl!("show-headerbar"),
config.show_headerbar,
Action::ShowHeaderBar(!config.show_headerbar),
)));
//If we have a link
//prepend the Open Link item
items.push(item(fl!("menu-password-manager"), Action::PasswordManager));
items.push(item(fl!("menu-settings"), Action::Settings));
items.push(item(fl!("menu-about"), Action::About));
// If we have a link, prepend the link items
if link.is_some() {
rows.insert(
0,
Element::from(menu_item(fl!("open-link"), Action::LaunchUrlByMenu)),
);
rows.insert(
1,
Element::from(menu_item(fl!("copy-link"), Action::CopyUrlByMenu)),
);
rows.insert(2, Element::from(divider::horizontal::light()));
items.insert(0, item(fl!("open-link"), Action::LaunchUrlByMenu));
items.insert(1, item(fl!("copy-link"), Action::CopyUrlByMenu));
items.insert(2, MenuItem::Divider);
}
let content = widget::menu::menu_column::MenuColumn::with_children(rows);
widget::container(content)
.padding(1)
//TODO: move style to libcosmic
.style(|theme| {
let cosmic = theme.cosmic();
let component = &cosmic.background(theme.transparent).component;
widget::container::Style {
icon_color: Some(component.on.into()),
text_color: Some(component.on.into()),
background: Some(Background::Color(component.base.into())),
border: Border {
radius: cosmic.radius_s().map(|x| x + 1.0).into(),
width: 1.0,
color: component.divider.into(),
},
..Default::default()
}
})
.width(Length::Fixed(360.0))
.into()
let key_binds: HashMap<KeyBind, TabAction> = key_binds
.iter()
.map(|(key_bind, action)| (key_bind.clone(), TabAction(entity, *action)))
.collect();
widget::menu::items(&key_binds, items)
}
pub fn color_scheme_menu<'a>(

View file

@ -218,11 +218,17 @@ impl TerminalPaneGrid {
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Metadata {
pub bg: cosmic_text::Color,
pub underline_color: cosmic_text::Color,
pub flags: Flags,
/// Remplissage natif (élément de bloc Unicode) — voir [`BlockFill`].
pub block: Option<BlockFill>,
/// Couleur du remplissage natif. Hors bloc, sans effet ; participant à
/// l'égalité pour que deux blocs adjacents de couleurs différentes ne
/// fusionnent pas en un seul quad.
pub block_color: cosmic_text::Color,
}
impl Metadata {
@ -232,6 +238,8 @@ impl Metadata {
bg,
underline_color,
flags,
block: None,
block_color: underline_color,
}
}
@ -245,6 +253,73 @@ impl Metadata {
fn with_flags(self, flags: Flags) -> Self {
Self { flags, ..self }
}
fn with_block(mut self, block: Option<BlockFill>, color: cosmic_text::Color) -> Self {
self.block = block;
self.block_color = color;
self
}
}
/// Élément de bloc Unicode (U+2580–U+259F) rendu nativement.
///
/// Ces caractères ne passent PAS par le glyphe de la police : le `█` d'une
/// police couvre à peine l'em (≈ 1× la taille de police) alors que la cellule
/// fait `line_height` (≈ 1,4×) — d'où des trous entre les rangées et un rendu
/// crénelé. À la place, chaque bloc est remplacé par une espace (le glyphe
/// disparaît) et peint en quad GPU couvrant exactement la fraction voulue de
/// la cellule : bords alignés sur la grille, jointif par construction, net à
/// toute échelle. C'est l'approche de kitty/WezTerm.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BlockFill {
/// Un ou deux rectangles en huitièmes de cellule : `(x0, x1, y0, y1)`,
/// abscisse et ordonnée depuis le coin haut-gauche. Un rectangle vide
/// (`x0 >= x1` ou `y0 >= y1`) est ignoré au tracé — deux rectangles
/// suffisent pour toute la famille des quadrants.
Rects([(u8, u8, u8, u8); 2]),
/// Trame (`░▒▓`) : rectangle plein, alpha en quarts (1–3).
Shade(u8),
}
/// Mappe un caractère vers son remplissage natif, s'il est un élément de bloc.
pub fn block_fill(c: char) -> Option<BlockFill> {
use BlockFill::{Rects, Shade};
const NONE: (u8, u8, u8, u8) = (8, 8, 8, 8);
match c {
'▀' => Some(Rects([(0, 8, 0, 4), NONE])),
'▁' => Some(Rects([(0, 8, 7, 8), NONE])),
'▂' => Some(Rects([(0, 8, 6, 8), NONE])),
'▃' => Some(Rects([(0, 8, 5, 8), NONE])),
'▄' => Some(Rects([(0, 8, 4, 8), NONE])),
'▅' => Some(Rects([(0, 8, 3, 8), NONE])),
'▆' => Some(Rects([(0, 8, 2, 8), NONE])),
'▇' => Some(Rects([(0, 8, 1, 8), NONE])),
'█' => Some(Rects([(0, 8, 0, 8), NONE])),
'▉' => Some(Rects([(0, 7, 0, 8), NONE])),
'▊' => Some(Rects([(0, 6, 0, 8), NONE])),
'▋' => Some(Rects([(0, 5, 0, 8), NONE])),
'▌' => Some(Rects([(0, 4, 0, 8), NONE])),
'▍' => Some(Rects([(0, 3, 0, 8), NONE])),
'▎' => Some(Rects([(0, 2, 0, 8), NONE])),
'▏' => Some(Rects([(0, 1, 0, 8), NONE])),
'▐' => Some(Rects([(4, 8, 0, 8), NONE])),
'░' => Some(Shade(1)),
'▒' => Some(Shade(2)),
'▓' => Some(Shade(3)),
'▔' => Some(Rects([(0, 8, 0, 1), NONE])),
'▕' => Some(Rects([(7, 8, 0, 8), NONE])),
'▖' => Some(Rects([(0, 4, 4, 8), NONE])),
'▗' => Some(Rects([(4, 8, 4, 8), NONE])),
'▘' => Some(Rects([(0, 4, 0, 4), NONE])),
'▝' => Some(Rects([(4, 8, 0, 4), NONE])),
'▙' => Some(Rects([(0, 4, 0, 8), (4, 8, 4, 8)])),
'▚' => Some(Rects([(0, 4, 0, 4), (4, 8, 4, 8)])),
'▛' => Some(Rects([(0, 8, 0, 4), (0, 4, 4, 8)])),
'▜' => Some(Rects([(0, 8, 0, 4), (4, 8, 4, 8)])),
'▞' => Some(Rects([(4, 8, 0, 4), (0, 4, 4, 8)])),
'▟' => Some(Rects([(0, 8, 4, 8), (4, 8, 0, 4)])),
_ => None,
}
}
pub struct Terminal {
@ -905,9 +980,14 @@ impl Terminal {
}
let start = text.len();
// Tab skip/stop is handled by alacritty_terminal
// Tab skip/stop is handled by alacritty_terminal.
// Les éléments de bloc ne deviennent pas des glyphes : la
// police ne couvre pas la cellule (trous d'interligne) ;
// ils sont peints en quads natifs au tracé, via Metadata.
let block = block_fill(indexed.cell.c);
text.push(match indexed.cell.c {
'\t' => ' ',
c if block.is_some() => ' ',
c => c,
});
if let Some(zerowidth) = indexed.cell.zerowidth() {
@ -1025,6 +1105,7 @@ impl Terminal {
}
let metadata = Metadata::new(bg, fg)
.with_block(block, fg)
.with_flags(flags)
.with_underline_color(underline_color);
let (meta_idx, _) = self.metadata_set.insert_full(metadata);
@ -1344,3 +1425,53 @@ impl Drop for Terminal {
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn block_fill_couvre_toute_la_plage_unicode() {
// U+2580–U+259F : chaque élément de bloc a sa géométrie native.
for code in 0x2580..=0x259F {
let c = char::from_u32(code).unwrap();
assert!(
block_fill(c).is_some(),
"{c} (U+{code:04X}) sans remplissage"
);
}
// Hors de la plage : filets, espaces, lettres — pas de remplissage.
for c in [' ', '─', '│', 'M', '●', '\u{25A0}'] {
assert!(
block_fill(c).is_none(),
"{c} ne doit pas être un bloc natif"
);
}
}
#[test]
fn block_fill_geometry_toujours_valide() {
// Fractions croissantes, bornées à la cellule — sinon le quad
// déborderait sur la cellule voisine ou serait rejeté au tracé.
for c in (char::from_u32(0x2580).unwrap()..=char::from_u32(0x259F).unwrap())
.chain(['░', '▒', '▓'].iter().copied())
{
let Some(fill) = block_fill(c) else { continue };
match fill {
BlockFill::Rects(rects) => {
for (x0, x1, y0, y1) in rects {
// Rectangle vide (sentinelle) ou borné à la cellule.
let empty = x0 >= x1 || y0 >= y1;
assert!(
empty || (x1 <= 8 && y1 <= 8),
"{c}: géométrie déborde la cellule"
);
}
}
BlockFill::Shade(quarters) => {
assert!((1..=3).contains(&quarters), "{c}: alpha de trame invalide")
}
}
}
}
}

View file

@ -46,7 +46,11 @@ use std::{
time::{Duration, Instant},
};
use crate::{Action, Terminal, TerminalScroll, menu::MenuState, terminal::Metadata};
use crate::{
Action, Terminal, TerminalScroll,
menu::MenuState,
terminal::{BlockFill, Metadata},
};
const AUTOSCROLL_INTERVAL: Duration = Duration::from_millis(100);
@ -112,7 +116,7 @@ pub struct TerminalBox<'a, Message> {
show_headerbar: bool,
pane_border_radius: Option<Radius>,
click_timing: Duration,
context_menu: Option<Point>,
context_menu_open: bool,
on_context_menu: Option<Box<dyn Fn(Option<MenuState>) -> Message + 'a>>,
on_mouse_enter: Option<Box<dyn Fn() -> Message + 'a>>,
opacity: Option<f32>,
@ -139,7 +143,7 @@ where
show_headerbar: true,
pane_border_radius: None,
click_timing: Duration::from_millis(500),
context_menu: None,
context_menu_open: false,
on_context_menu: None,
on_mouse_enter: None,
opacity: None,
@ -184,8 +188,8 @@ where
self
}
pub fn context_menu(mut self, position: Point) -> Self {
self.context_menu = Some(position);
pub fn context_menu_open(mut self, open: bool) -> Self {
self.context_menu_open = open;
self
}
@ -249,7 +253,7 @@ where
if !state.is_focused {
return InputMethod::Disabled;
}
if self.context_menu.is_some() {
if self.context_menu_open {
return InputMethod::Disabled;
}
@ -590,6 +594,41 @@ where
);
}
// Éléments de bloc peints nativement : fractions de la
// cellule (ou de la course fusionnée, à géométrie
// séparable), bords calés sur la grille — zéro glyphe,
// zéro interligne.
if let Some(block) = metadata.block {
let span_w = self.end_x - self.start_x;
let mut color = cosmic_text_to_iced_color(metadata.block_color);
let rects = match block {
BlockFill::Rects(rects) => rects,
BlockFill::Shade(quarters) => {
color.a *= f32::from(quarters) / 4.0;
[(0, 8, 0, 8), (8, 8, 8, 8)]
}
};
for (x0, x1, y0, y1) in rects {
if x0 >= x1 || y0 >= y1 {
continue;
}
let left = self.start_x + span_w * f32::from(x0) / 8.0;
let right = self.start_x + span_w * f32::from(x1) / 8.0;
let top = self.line_top + self.line_height * f32::from(y0) / 8.0;
let bottom = self.line_top + self.line_height * f32::from(y1) / 8.0;
renderer.fill_quad(
Quad {
bounds: Rectangle::new(
self.view_position + Vector::new(left.floor(), top),
Size::new(right.ceil() - left.floor(), bottom - top),
),
..Default::default()
},
color,
);
}
}
if !metadata.flags.is_empty() {
let style_line_height = (self.glyph_font_size / 10.0).clamp(1.0, 16.0);
@ -1353,11 +1392,11 @@ where
}
// Update context menu state
if let Some(on_context_menu) = &self.on_context_menu {
match self.context_menu {
Some(_) => {
match self.context_menu_open {
true => {
shell.publish(on_context_menu(None));
}
None => {
false => {
if *button == Button::Right {
let x = p.x - self.padding.left;
let y = p.y - self.padding.top;
@ -1375,15 +1414,7 @@ where
None,
);
let link = get_hyperlink(&terminal, location);
let abs = cosmic::iced::Point::new(
layout.bounds().x + p.x,
layout.bounds().y + p.y,
);
shell.publish(on_context_menu(Some(MenuState {
position: Some(abs),
local_position: Some(p),
link,
})));
shell.publish(on_context_menu(Some(MenuState { link })));
}
}
}