Compare commits
No commits in common. "81dc7d3eebb9bccc2ed5f603fb0a3721a3bed298" and "99842bde03c134cb56cf1b4515d257609f6af3dd" have entirely different histories.
81dc7d3eeb
...
99842bde03
16 changed files with 642 additions and 370 deletions
501
Cargo.lock
generated
501
Cargo.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "cosmic-term"
|
name = "cosmic-term"
|
||||||
version = "1.9.0"
|
version = "1.8.0"
|
||||||
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
authors = ["Jeremy Soller <jeremy@system76.com>"]
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "GPL-3.0-only"
|
license = "GPL-3.0-only"
|
||||||
|
|
|
||||||
6
debian/changelog
vendored
6
debian/changelog
vendored
|
|
@ -1,9 +1,3 @@
|
||||||
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
|
cosmic-term (1.8.0) noble; urgency=medium
|
||||||
|
|
||||||
* Epoch 1.8.0 version update
|
* Epoch 1.8.0 version update
|
||||||
|
|
|
||||||
6
debian/source/options
vendored
6
debian/source/options
vendored
|
|
@ -1,6 +1,4 @@
|
||||||
tar-ignore=.github
|
tar-ignore=.github
|
||||||
tar-ignore=target/.rustc_info.json
|
tar-ignore=.vscode
|
||||||
tar-ignore=target/CACHEDIR.TAG
|
|
||||||
tar-ignore=target/debug
|
|
||||||
tar-ignore=target/release
|
|
||||||
tar-ignore=vendor
|
tar-ignore=vendor
|
||||||
|
tar-ignore=target
|
||||||
|
|
@ -63,7 +63,6 @@ show-pane-borders = Show pane borders
|
||||||
### Advanced
|
### Advanced
|
||||||
advanced = Advanced
|
advanced = Advanced
|
||||||
show-headerbar = Show header
|
show-headerbar = Show header
|
||||||
window-header = Window header
|
|
||||||
show-header-description = Reveal the header from the right-click menu
|
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 = 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
|
tab-new-inherit-working-directory-description = Open new tabs and windows in the active tab's working directory
|
||||||
|
|
@ -153,7 +152,7 @@ next-tab = Next tab
|
||||||
previous-tab = Previous tab
|
previous-tab = Previous tab
|
||||||
split-horizontal = Split horizontal
|
split-horizontal = Split horizontal
|
||||||
split-vertical = Split vertical
|
split-vertical = Split vertical
|
||||||
pane-toggle-maximize = Maximize or restore pane
|
pane-toggle-maximize = Toggle maximized
|
||||||
menu-color-schemes = Color schemes...
|
menu-color-schemes = Color schemes...
|
||||||
menu-settings = Settings...
|
menu-settings = Settings...
|
||||||
menu-about = About COSMIC Terminal...
|
menu-about = About COSMIC Terminal...
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ repository = Repositori
|
||||||
support = Dukungan
|
support = Dukungan
|
||||||
color-schemes = Skema warna
|
color-schemes = Skema warna
|
||||||
cosmic-terminal = Terminal COSMIC
|
cosmic-terminal = Terminal COSMIC
|
||||||
rename = Ganti Nama
|
rename = Ganti nama
|
||||||
export = Ekspor
|
export = Ekspor
|
||||||
delete = Hapus
|
delete = Hapus
|
||||||
import = Impor
|
import = Impor
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
cancel = Semmet
|
cancel = Sefsex
|
||||||
name = Isem
|
name = Isem
|
||||||
delete = kkes
|
delete = kkes
|
||||||
rename = Snifel isem
|
rename = Snifel isem
|
||||||
quit = Ffeɣ
|
quit = Tuffɣa
|
||||||
support = Tallelt
|
support = Tallalt
|
||||||
repository = Asarsay
|
repository = Asarsay
|
||||||
settings = Iɣewwaṛen
|
settings = Iɣewwaṛen
|
||||||
appearance = Timeẓri
|
appearance = Timeẓri
|
||||||
|
|
@ -14,7 +14,7 @@ paste = Senteḍ
|
||||||
select-all = Fren akk
|
select-all = Fren akk
|
||||||
view = Wali
|
view = Wali
|
||||||
menu-settings = Iɣewwaṛen…
|
menu-settings = Iɣewwaṛen…
|
||||||
match-desktop = Semṣada akked tnarit
|
match-desktop = Amṣada d tnarit
|
||||||
file = Afaylu
|
file = Afaylu
|
||||||
new-window = Asfaylu amaynut
|
new-window = Asfaylu amaynut
|
||||||
edit = Ẓreg
|
edit = Ẓreg
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ light = Ronî
|
||||||
advanced = Pêşketî
|
advanced = Pêşketî
|
||||||
support = Piştgirî
|
support = Piştgirî
|
||||||
default-font = Cûrenivîs
|
default-font = Cûrenivîs
|
||||||
paste = Pê ve bike
|
paste = Pêve bike
|
||||||
menu-settings = Sazkarî...
|
menu-settings = Sazkarî...
|
||||||
command-line = Rêza fermanê
|
command-line = Rêza fermanê
|
||||||
view = Dîtin
|
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-right = Balê bikişîne ser destgeha rastê
|
||||||
focus-pane-up = Balê bikişîne ser destgeha jorîn
|
focus-pane-up = Balê bikişîne ser destgeha jorîn
|
||||||
password-manager = Rêveberê borînpeyvê
|
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
|
reset-to-default = Vegere rewşa berdest
|
||||||
shortcut-capture-hint = Kombînasyona kilîtan bitikîne
|
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î?
|
shortcut-replace-body = { $binding } berê ji bo { $existing } hatiye destnîşankirin. Tu dixwazî bi { $new_action } re biguherînî?
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
settings = Configuracion
|
settings = Paramètres
|
||||||
appearance = Aparéncia
|
appearance = Aparéncia
|
||||||
theme = Tèma
|
theme = Tèma
|
||||||
match-desktop = Assortir al burèu
|
match-desktop = Assortir al burèu
|
||||||
dark = Escur
|
dark = Fosc
|
||||||
light = Clar
|
light = Clar
|
||||||
file = Fichièr
|
file = Fichièr
|
||||||
name = Nom
|
|
||||||
repository = Depaus
|
|
||||||
cancel = Anullar
|
|
||||||
menu-settings = Configuracion...
|
|
||||||
quit = Quitar
|
|
||||||
delete = Suprimir
|
|
||||||
replace = Remplaçar
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
cosmic-terminal = Terminal COSMIC
|
cosmic-terminal = Terminal COSMIC
|
||||||
comment = Emulator terminala dla środowiska COSMIC
|
comment = Emulator terminala dla pulpitu COSMIC
|
||||||
keywords = Cli;WierszPoleceń;Powłoka;Terminal;
|
keywords = Cli;WierszPoleceń;Powłoka;Terminal;
|
||||||
new-terminal = Nowy terminal
|
new-terminal = Nowy terminal
|
||||||
|
|
||||||
|
|
@ -73,7 +73,7 @@ advanced = Zaawansowane
|
||||||
show-headerbar = Pokaż pasek nagłówka
|
show-headerbar = Pokaż pasek nagłówka
|
||||||
show-header-description = Ukazuje pasek nagłówka z menu wybieranego prawym przyciskiem myszki
|
show-header-description = Ukazuje pasek nagłówka z menu wybieranego prawym przyciskiem myszki
|
||||||
# Find
|
# Find
|
||||||
find-placeholder = Wyszukaj…
|
find-placeholder = Szukaj…
|
||||||
find-previous = Znajdź poprzedni
|
find-previous = Znajdź poprzedni
|
||||||
find-next = Znajdź następny
|
find-next = Znajdź następny
|
||||||
|
|
||||||
|
|
@ -96,7 +96,7 @@ edit = Edytuj
|
||||||
copy = Kopiuj
|
copy = Kopiuj
|
||||||
paste = Wklej
|
paste = Wklej
|
||||||
select-all = Zaznacz wszystko
|
select-all = Zaznacz wszystko
|
||||||
find = Wyszukaj
|
find = Szukaj
|
||||||
clear-scrollback = Wyczyść przewijanie
|
clear-scrollback = Wyczyść przewijanie
|
||||||
|
|
||||||
## View
|
## View
|
||||||
|
|
|
||||||
|
|
@ -39,8 +39,8 @@ settings = การตั้งค่า
|
||||||
appearance = ลักษณะ
|
appearance = ลักษณะ
|
||||||
theme = ธีม
|
theme = ธีม
|
||||||
match-desktop = ใช้ตามธีมหน้าจอหลัก
|
match-desktop = ใช้ตามธีมหน้าจอหลัก
|
||||||
dark = มืด
|
dark = ธีมมืด
|
||||||
light = สว่าง
|
light = ธีมสว่าง
|
||||||
syntax-dark = โทนสีมืด
|
syntax-dark = โทนสีมืด
|
||||||
syntax-light = โทนสีสว่าง
|
syntax-light = โทนสีสว่าง
|
||||||
default-zoom-step = ระยะซูม
|
default-zoom-step = ระยะซูม
|
||||||
|
|
@ -109,8 +109,3 @@ pane-toggle-maximize = ขยายให้ใหญ่สุด
|
||||||
menu-color-schemes = โทนสี...
|
menu-color-schemes = โทนสี...
|
||||||
menu-settings = การตั้งค่า...
|
menu-settings = การตั้งค่า...
|
||||||
menu-about = เกี่ยวกับ เทอร์มินัล COSMIC...
|
menu-about = เกี่ยวกับ เทอร์มินัล COSMIC...
|
||||||
cancel = ยกเลิก
|
|
||||||
reset-to-default = รีเซ็ตเป็นค่าเริ่มต้น
|
|
||||||
type-to-search = พิมพ์เพื่อค้นหา
|
|
||||||
add-another-keybinding = เพิ่มคีย์ลัดเพิ่มเติม
|
|
||||||
replace = แทนที่
|
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ new-terminal = 新建終端機
|
||||||
## Color schemes
|
## Color schemes
|
||||||
|
|
||||||
color-schemes = 配色方案
|
color-schemes = 配色方案
|
||||||
rename = 重新命名
|
rename = 重命名
|
||||||
export = 匯出
|
export = 匯出
|
||||||
delete = 刪除
|
delete = 刪除
|
||||||
import = 匯入
|
import = 匯入
|
||||||
|
|
|
||||||
241
src/main.rs
241
src/main.rs
|
|
@ -566,7 +566,7 @@ pub enum Message {
|
||||||
ReorderTab(Pane, ReorderEvent),
|
ReorderTab(Pane, ReorderEvent),
|
||||||
SaveOutput(Option<segmented_button::Entity>),
|
SaveOutput(Option<segmented_button::Entity>),
|
||||||
SaveOutputResult(pane_grid::Pane, segmented_button::Entity, DialogResult),
|
SaveOutputResult(pane_grid::Pane, segmented_button::Entity, DialogResult),
|
||||||
Surface(surface::Action<Message>),
|
Surface(surface::Action),
|
||||||
SelectAll(Option<segmented_button::Entity>),
|
SelectAll(Option<segmented_button::Entity>),
|
||||||
ShowAdvancedFontSettings(bool),
|
ShowAdvancedFontSettings(bool),
|
||||||
ShowHeaderBar(bool),
|
ShowHeaderBar(bool),
|
||||||
|
|
@ -604,6 +604,7 @@ pub enum Message {
|
||||||
ZoomIn,
|
ZoomIn,
|
||||||
ZoomOut,
|
ZoomOut,
|
||||||
ZoomReset,
|
ZoomReset,
|
||||||
|
ContextMenuPopupClosed(window::Id),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||||
|
|
@ -681,6 +682,14 @@ pub struct App {
|
||||||
shortcut_search_regex: Option<regex::Regex>,
|
shortcut_search_regex: Option<regex::Regex>,
|
||||||
shortcut_search_value: String,
|
shortcut_search_value: String,
|
||||||
modifiers: Modifiers,
|
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")]
|
#[cfg(feature = "password_manager")]
|
||||||
password_mgr: password_manager::PasswordManager,
|
password_mgr: password_manager::PasswordManager,
|
||||||
}
|
}
|
||||||
|
|
@ -2204,6 +2213,7 @@ impl Application for App {
|
||||||
shortcut_search_regex: None,
|
shortcut_search_regex: None,
|
||||||
shortcut_search_value: String::new(),
|
shortcut_search_value: String::new(),
|
||||||
modifiers: Modifiers::empty(),
|
modifiers: Modifiers::empty(),
|
||||||
|
context_menu_popup: None,
|
||||||
#[cfg(feature = "password_manager")]
|
#[cfg(feature = "password_manager")]
|
||||||
password_mgr: Default::default(),
|
password_mgr: Default::default(),
|
||||||
};
|
};
|
||||||
|
|
@ -3423,7 +3433,16 @@ impl Application for App {
|
||||||
return self.update_title(None);
|
return self.update_title(None);
|
||||||
}
|
}
|
||||||
Message::TabContextAction(entity, action) => {
|
Message::TabContextAction(entity, action) => {
|
||||||
|
// Close context menu popup
|
||||||
let mut tasks = Vec::new();
|
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
|
// Close terminal context menu state
|
||||||
if let Some(tab_model) = self.pane_model.active()
|
if let Some(tab_model) = self.pane_model.active()
|
||||||
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
|
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
|
||||||
|
|
@ -3432,8 +3451,11 @@ impl Application for App {
|
||||||
//Some actions need the menu_state,
|
//Some actions need the menu_state,
|
||||||
//so only clear the position for them.
|
//so only clear the position for them.
|
||||||
match action {
|
match action {
|
||||||
// these read the link from the menu state and clear it themselves
|
Action::LaunchUrlByMenu | Action::CopyUrlByMenu => {
|
||||||
Action::LaunchUrlByMenu | Action::CopyUrlByMenu => {}
|
if let Some(context_menu) = terminal.context_menu.as_mut() {
|
||||||
|
context_menu.position = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
_ => {
|
_ => {
|
||||||
terminal.context_menu = None;
|
terminal.context_menu = None;
|
||||||
}
|
}
|
||||||
|
|
@ -3443,29 +3465,94 @@ impl Application for App {
|
||||||
return cosmic::Task::batch(tasks);
|
return cosmic::Task::batch(tasks);
|
||||||
}
|
}
|
||||||
Message::TabContextMenu(pane, menu_state) => {
|
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
|
// Clear all terminal context_menu state
|
||||||
for (_, tab_model) in self.pane_model.panes.iter() {
|
for (_, tab_model) in self.pane_model.panes.iter() {
|
||||||
for entity in tab_model.iter() {
|
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();
|
||||||
if terminal.context_menu.take().is_some() {
|
terminal.context_menu = None;
|
||||||
terminal.active_regex_match = None;
|
|
||||||
terminal.needs_update = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// A right press records what is under the cursor
|
if let Some(menu_state) = menu_state {
|
||||||
if let Some(menu_state) = menu_state
|
if let Some(_position) = menu_state.position {
|
||||||
&& let Some(tab_model) = self.pane_model.panes.get(pane)
|
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 entity = tab_model.active();
|
||||||
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
let link = menu_state.link.clone();
|
||||||
terminal.lock().unwrap().context_menu = Some(menu_state);
|
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),
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.pane_model.set_focus(pane);
|
self.pane_model.set_focus(pane);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return cosmic::Task::batch(tasks);
|
||||||
}
|
}
|
||||||
Message::TabNew => {
|
Message::TabNew => {
|
||||||
// The profile from `--profile` only applies to the first tab
|
// The profile from `--profile` only applies to the first tab
|
||||||
|
|
@ -3857,8 +3944,26 @@ impl Application for App {
|
||||||
self.reset_active_pane_zoom();
|
self.reset_active_pane_zoom();
|
||||||
return self.update_config();
|
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) => {
|
Message::Surface(a) => {
|
||||||
return cosmic::task::message(cosmic::Action::Surface(a));
|
return cosmic::task::message(cosmic::Action::Cosmic(
|
||||||
|
cosmic::app::Action::Surface(a),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Message::ReorderTab(
|
Message::ReorderTab(
|
||||||
pane,
|
pane,
|
||||||
|
|
@ -4007,7 +4112,32 @@ 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> {
|
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 {
|
match &self.dialog_opt {
|
||||||
Some(dialog) => dialog.view(window_id),
|
Some(dialog) => dialog.view(window_id),
|
||||||
None => widget::text("Unknown window ID").into(),
|
None => widget::text("Unknown window ID").into(),
|
||||||
|
|
@ -4096,36 +4226,51 @@ impl Application for App {
|
||||||
terminal_box = terminal_box.on_mouse_enter(move || Message::MouseEnter(pane));
|
terminal_box = terminal_box.on_mouse_enter(move || Message::MouseEnter(pane));
|
||||||
}
|
}
|
||||||
|
|
||||||
// The terminal records what was under the right press; the widget opens the
|
// If a context menu popup is active for this pane, inform the
|
||||||
// menu on release, so build it from that state
|
// terminal_box so it will emit on_context_menu(None) on click
|
||||||
let context_link = terminal
|
// to dismiss the popup.
|
||||||
.lock()
|
if self.context_menu_popup.is_some() {
|
||||||
.unwrap()
|
terminal_box = terminal_box.context_menu(cosmic::iced::Point::ORIGIN);
|
||||||
.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 mut context_menu = widget::context_menu(
|
|
||||||
terminal_box,
|
let use_wayland_popup = {
|
||||||
context_link.map(|link| {
|
#[cfg(feature = "wayland")]
|
||||||
menu::context_menu(
|
{
|
||||||
&self.config,
|
is_wayland()
|
||||||
&self.key_binds,
|
}
|
||||||
entity,
|
#[cfg(not(feature = "wayland"))]
|
||||||
link,
|
{
|
||||||
self.terminal_monitors(pane, entity),
|
false
|
||||||
)
|
}
|
||||||
}),
|
};
|
||||||
)
|
|
||||||
.item_width(widget::menu::ItemWidth::Uniform(360))
|
let tab_element: Element<'_, Message> = if !use_wayland_popup {
|
||||||
.on_close(Message::TabContextMenu(pane, None))
|
// Fallback: render context menu as an inline popover
|
||||||
.on_surface_action(Message::Surface);
|
if let Some((_, popup_pane, popup_entity, ref link, _, point)) =
|
||||||
if let Some(window_id) = self.core.main_window_id() {
|
self.context_menu_popup
|
||||||
context_menu = context_menu.window_id(window_id);
|
{
|
||||||
}
|
if pane == popup_pane {
|
||||||
tab_column = tab_column.push(context_menu);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
//Only draw find in the currently focused pane
|
//Only draw find in the currently focused pane
|
||||||
|
|
@ -4306,6 +4451,14 @@ 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.
|
/// Divider color painted behind the pane grid to form pane borders.
|
||||||
///
|
///
|
||||||
/// When blur is active the transparent container must be used, otherwise the
|
/// When blur is active the transparent container must be used, otherwise the
|
||||||
|
|
|
||||||
185
src/menu.rs
185
src/menu.rs
|
|
@ -1,15 +1,17 @@
|
||||||
// SPDX-License-Identifier: GPL-3.0-only
|
// SPDX-License-Identifier: GPL-3.0-only
|
||||||
|
|
||||||
use cosmic::widget::menu::action::MenuAction;
|
use cosmic::iced::Point;
|
||||||
use cosmic::widget::menu::key_bind::KeyBind;
|
use cosmic::widget::menu::key_bind::KeyBind;
|
||||||
use cosmic::widget::menu::{Item as MenuItem, menu_button};
|
use cosmic::widget::menu::{Item as MenuItem, menu_button};
|
||||||
|
use cosmic::widget::space;
|
||||||
use cosmic::{
|
use cosmic::{
|
||||||
Element,
|
Element,
|
||||||
app::Core,
|
app::Core,
|
||||||
iced::core::Border,
|
iced::core::Border,
|
||||||
iced::{Background, Length},
|
iced::{Background, Length, advanced::widget::text::Style as TextStyle},
|
||||||
|
theme,
|
||||||
widget::{
|
widget::{
|
||||||
self,
|
self, divider,
|
||||||
menu::{ItemHeight, ItemWidth},
|
menu::{ItemHeight, ItemWidth},
|
||||||
responsive_menu_bar, segmented_button,
|
responsive_menu_bar, segmented_button,
|
||||||
},
|
},
|
||||||
|
|
@ -21,92 +23,147 @@ use crate::{Action, ColorSchemeId, ColorSchemeKind, Config, Message, fl, termina
|
||||||
static MENU_ID: LazyLock<cosmic::widget::Id> =
|
static MENU_ID: LazyLock<cosmic::widget::Id> =
|
||||||
LazyLock::new(|| cosmic::widget::Id::new("responsive-menu"));
|
LazyLock::new(|| cosmic::widget::Id::new("responsive-menu"));
|
||||||
|
|
||||||
/// What the terminal found under the right-click
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct MenuState {
|
pub struct MenuState {
|
||||||
|
pub position: Option<Point>,
|
||||||
|
pub local_position: Option<Point>,
|
||||||
pub link: Option<String>,
|
pub link: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A context menu action dispatched to the tab the menu was opened on.
|
pub fn context_menu<'a>(
|
||||||
#[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,
|
config: &Config,
|
||||||
key_binds: &HashMap<KeyBind, Action>,
|
key_binds: &HashMap<KeyBind, Action>,
|
||||||
entity: segmented_button::Entity,
|
entity: segmented_button::Entity,
|
||||||
link: Option<String>,
|
link: Option<String>,
|
||||||
monitors: (bool, bool, bool),
|
monitors: (bool, bool, bool),
|
||||||
) -> Vec<widget::menu::Tree<Message>> {
|
) -> Element<'a, Message> {
|
||||||
let item =
|
let find_key = |action: &Action| -> String {
|
||||||
|label: String, action: Action| MenuItem::Button(label, None, TabAction(entity, action));
|
for (key_bind, key_action) in key_binds {
|
||||||
let check = |label: String, value: bool, action: Action| {
|
if action == key_action {
|
||||||
MenuItem::CheckBox(label, None, value, TabAction(entity, 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))
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut items = vec![
|
let menu_checkbox = |label, value, action| {
|
||||||
item(fl!("new-tab"), Action::TabNew),
|
menu_button(vec![
|
||||||
item(fl!("rename-tab"), Action::TabRename),
|
widget::text(label).into(),
|
||||||
MenuItem::Divider,
|
widget::space::horizontal().into(),
|
||||||
item(fl!("copy"), Action::Copy),
|
widget::toggler(value)
|
||||||
item(fl!("paste"), Action::Paste),
|
.on_toggle(move |_| Message::TabContextAction(entity, action))
|
||||||
item(fl!("select-all"), Action::SelectAll),
|
.size(16.0)
|
||||||
MenuItem::Divider,
|
.into(),
|
||||||
item(fl!("clear-scrollback"), Action::ClearScrollback),
|
])
|
||||||
item(fl!("save-output"), Action::SaveOutput),
|
.on_press(Message::TabContextAction(entity, action))
|
||||||
MenuItem::Divider,
|
};
|
||||||
check(
|
|
||||||
|
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(
|
||||||
fl!("monitor-activity"),
|
fl!("monitor-activity"),
|
||||||
monitors.0,
|
monitors.0,
|
||||||
Action::Monitor(MonitorKind::Activity),
|
Action::Monitor(MonitorKind::Activity),
|
||||||
),
|
)),
|
||||||
check(
|
Element::from(menu_checkbox(
|
||||||
fl!("monitor-silence"),
|
fl!("monitor-silence"),
|
||||||
monitors.1,
|
monitors.1,
|
||||||
Action::Monitor(MonitorKind::Silence),
|
Action::Monitor(MonitorKind::Silence),
|
||||||
),
|
)),
|
||||||
check(
|
Element::from(menu_checkbox(
|
||||||
fl!("monitor-process-finished"),
|
fl!("monitor-process-finished"),
|
||||||
monitors.2,
|
monitors.2,
|
||||||
Action::Monitor(MonitorKind::ProcessFinished),
|
Action::Monitor(MonitorKind::ProcessFinished),
|
||||||
),
|
)),
|
||||||
MenuItem::Divider,
|
Element::from(divider::horizontal::light()),
|
||||||
item(fl!("split-horizontal"), Action::PaneSplitHorizontal),
|
Element::from(menu_item(
|
||||||
item(fl!("split-vertical"), Action::PaneSplitVertical),
|
fl!("split-horizontal"),
|
||||||
item(fl!("pane-toggle-maximize"), Action::PaneToggleMaximized),
|
Action::PaneSplitHorizontal,
|
||||||
MenuItem::Divider,
|
)),
|
||||||
MenuItem::CheckBox(
|
Element::from(menu_item(fl!("split-vertical"), Action::PaneSplitVertical)),
|
||||||
fl!("window-header"),
|
Element::from(menu_item(
|
||||||
None,
|
fl!("pane-toggle-maximize"),
|
||||||
config.show_headerbar,
|
Action::PaneToggleMaximized,
|
||||||
TabAction(entity, Action::ShowHeaderBar(!config.show_headerbar)),
|
)),
|
||||||
),
|
Element::from(divider::horizontal::light()),
|
||||||
MenuItem::Divider,
|
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)),
|
||||||
];
|
];
|
||||||
#[cfg(feature = "password_manager")]
|
#[cfg(feature = "password_manager")]
|
||||||
items.push(item(fl!("menu-password-manager"), Action::PasswordManager));
|
{
|
||||||
items.push(item(fl!("menu-settings"), Action::Settings));
|
rows.push(Element::from(menu_item(
|
||||||
items.push(item(fl!("menu-about"), Action::About));
|
fl!("menu-password-manager"),
|
||||||
// If we have a link, prepend the link items
|
Action::PasswordManager,
|
||||||
if link.is_some() {
|
)));
|
||||||
items.insert(0, item(fl!("open-link"), Action::LaunchUrlByMenu));
|
|
||||||
items.insert(1, item(fl!("copy-link"), Action::CopyUrlByMenu));
|
|
||||||
items.insert(2, MenuItem::Divider);
|
|
||||||
}
|
}
|
||||||
|
rows.push(Element::from(menu_checkbox(
|
||||||
|
fl!("show-headerbar"),
|
||||||
|
config.show_headerbar,
|
||||||
|
Action::ShowHeaderBar(!config.show_headerbar),
|
||||||
|
)));
|
||||||
|
|
||||||
let key_binds: HashMap<KeyBind, TabAction> = key_binds
|
//If we have a link
|
||||||
.iter()
|
//prepend the Open Link item
|
||||||
.map(|(key_bind, action)| (key_bind.clone(), TabAction(entity, *action)))
|
if link.is_some() {
|
||||||
.collect();
|
rows.insert(
|
||||||
widget::menu::items(&key_binds, items)
|
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()));
|
||||||
|
}
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn color_scheme_menu<'a>(
|
pub fn color_scheme_menu<'a>(
|
||||||
|
|
|
||||||
|
|
@ -116,7 +116,7 @@ pub struct TerminalBox<'a, Message> {
|
||||||
show_headerbar: bool,
|
show_headerbar: bool,
|
||||||
pane_border_radius: Option<Radius>,
|
pane_border_radius: Option<Radius>,
|
||||||
click_timing: Duration,
|
click_timing: Duration,
|
||||||
context_menu_open: bool,
|
context_menu: Option<Point>,
|
||||||
on_context_menu: Option<Box<dyn Fn(Option<MenuState>) -> Message + 'a>>,
|
on_context_menu: Option<Box<dyn Fn(Option<MenuState>) -> Message + 'a>>,
|
||||||
on_mouse_enter: Option<Box<dyn Fn() -> Message + 'a>>,
|
on_mouse_enter: Option<Box<dyn Fn() -> Message + 'a>>,
|
||||||
opacity: Option<f32>,
|
opacity: Option<f32>,
|
||||||
|
|
@ -143,7 +143,7 @@ where
|
||||||
show_headerbar: true,
|
show_headerbar: true,
|
||||||
pane_border_radius: None,
|
pane_border_radius: None,
|
||||||
click_timing: Duration::from_millis(500),
|
click_timing: Duration::from_millis(500),
|
||||||
context_menu_open: false,
|
context_menu: None,
|
||||||
on_context_menu: None,
|
on_context_menu: None,
|
||||||
on_mouse_enter: None,
|
on_mouse_enter: None,
|
||||||
opacity: None,
|
opacity: None,
|
||||||
|
|
@ -188,8 +188,8 @@ where
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn context_menu_open(mut self, open: bool) -> Self {
|
pub fn context_menu(mut self, position: Point) -> Self {
|
||||||
self.context_menu_open = open;
|
self.context_menu = Some(position);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,7 +253,7 @@ where
|
||||||
if !state.is_focused {
|
if !state.is_focused {
|
||||||
return InputMethod::Disabled;
|
return InputMethod::Disabled;
|
||||||
}
|
}
|
||||||
if self.context_menu_open {
|
if self.context_menu.is_some() {
|
||||||
return InputMethod::Disabled;
|
return InputMethod::Disabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1392,11 +1392,11 @@ where
|
||||||
}
|
}
|
||||||
// Update context menu state
|
// Update context menu state
|
||||||
if let Some(on_context_menu) = &self.on_context_menu {
|
if let Some(on_context_menu) = &self.on_context_menu {
|
||||||
match self.context_menu_open {
|
match self.context_menu {
|
||||||
true => {
|
Some(_) => {
|
||||||
shell.publish(on_context_menu(None));
|
shell.publish(on_context_menu(None));
|
||||||
}
|
}
|
||||||
false => {
|
None => {
|
||||||
if *button == Button::Right {
|
if *button == Button::Right {
|
||||||
let x = p.x - self.padding.left;
|
let x = p.x - self.padding.left;
|
||||||
let y = p.y - self.padding.top;
|
let y = p.y - self.padding.top;
|
||||||
|
|
@ -1414,7 +1414,15 @@ where
|
||||||
None,
|
None,
|
||||||
);
|
);
|
||||||
let link = get_hyperlink(&terminal, location);
|
let link = get_hyperlink(&terminal, location);
|
||||||
shell.publish(on_context_menu(Some(MenuState { link })));
|
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,
|
||||||
|
})));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue