2023-12-20 13:31:10 -07:00
|
|
|
// Copyright 2023 System76 <info@system76.com>
|
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
|
2024-03-13 09:12:07 -06:00
|
|
|
use alacritty_terminal::{event::Event as TermEvent, term, term::color::Colors as TermColors, tty};
|
2024-08-03 20:33:55 -06:00
|
|
|
use cosmic::iced::clipboard::dnd::DndAction;
|
2026-04-08 15:30:10 -06:00
|
|
|
use cosmic::iced::core::keyboard::key::Named;
|
2026-05-26 15:40:59 -04:00
|
|
|
use cosmic::iced::keyboard::key::Physical;
|
2024-03-18 19:31:28 -07:00
|
|
|
use cosmic::widget::menu::action::MenuAction;
|
|
|
|
|
use cosmic::widget::menu::key_bind::KeyBind;
|
2026-01-28 20:08:44 -05:00
|
|
|
use cosmic::widget::pane_grid::Pane;
|
|
|
|
|
use cosmic::widget::segmented_button::ReorderEvent;
|
2023-12-20 13:31:10 -07:00
|
|
|
use cosmic::{
|
2025-09-10 14:33:46 +02:00
|
|
|
Application, ApplicationExt, Element, action,
|
|
|
|
|
app::{Core, Settings, Task, context_drawer},
|
2024-02-09 15:45:46 -07:00
|
|
|
cosmic_config::{self, ConfigSet, CosmicConfigEntry},
|
2023-12-20 13:31:10 -07:00
|
|
|
cosmic_theme, executor,
|
|
|
|
|
iced::{
|
2025-10-16 09:27:00 +02:00
|
|
|
self, Alignment, Color, Event, Length, Limits, Padding, Subscription,
|
2023-12-22 14:31:01 -07:00
|
|
|
advanced::graphics::text::font_system,
|
2023-12-21 22:13:17 -07:00
|
|
|
clipboard, event,
|
2023-12-20 13:31:10 -07:00
|
|
|
futures::SinkExt,
|
2024-02-05 14:18:19 -05:00
|
|
|
keyboard::{Event as KeyEvent, Key, Modifiers},
|
2024-01-21 21:13:13 +01:00
|
|
|
mouse::{Button as MouseButton, Event as MouseEvent},
|
2025-09-10 14:33:46 +02:00
|
|
|
stream, window,
|
2023-12-17 22:51:50 -07:00
|
|
|
},
|
2023-12-20 13:31:10 -07:00
|
|
|
style,
|
2025-09-11 16:25:29 +02:00
|
|
|
widget::{self, DndDestination, PaneGrid, about::About, button, pane_grid, segmented_button},
|
2023-12-17 17:49:39 -07:00
|
|
|
};
|
2025-09-10 14:33:46 +02:00
|
|
|
use cosmic::{Apply, surface};
|
2025-07-01 09:41:36 -06:00
|
|
|
use cosmic_files::dialog::{Dialog, DialogKind, DialogMessage, DialogResult, DialogSettings};
|
2025-09-10 14:33:46 +02:00
|
|
|
use cosmic_text::{Family, Stretch, Weight, fontdb::FaceInfo};
|
2024-07-10 03:22:23 -04:00
|
|
|
use localize::LANGUAGE_SORTER;
|
2024-01-11 11:33:39 -07:00
|
|
|
use std::{
|
|
|
|
|
any::TypeId,
|
2026-02-05 11:25:53 -07:00
|
|
|
cell::Cell,
|
2024-02-12 12:36:30 -07:00
|
|
|
cmp,
|
2024-01-11 11:33:39 -07:00
|
|
|
collections::{BTreeMap, BTreeSet, HashMap},
|
2025-07-13 15:50:37 +03:00
|
|
|
env,
|
|
|
|
|
error::Error,
|
2026-02-16 23:32:19 +00:00
|
|
|
fs,
|
|
|
|
|
path::PathBuf,
|
|
|
|
|
process,
|
2024-10-22 12:31:02 -06:00
|
|
|
rc::Rc,
|
2025-09-10 14:33:46 +02:00
|
|
|
sync::{LazyLock, Mutex, atomic::Ordering},
|
2024-01-11 11:33:39 -07:00
|
|
|
};
|
2023-12-20 13:31:10 -07:00
|
|
|
use tokio::sync::mpsc;
|
2023-12-17 11:53:26 -07:00
|
|
|
|
2024-02-22 11:18:13 -07:00
|
|
|
use config::{
|
2025-09-10 14:33:46 +02:00
|
|
|
AppTheme, CONFIG_VERSION, ColorScheme, ColorSchemeId, ColorSchemeKind, Config, Profile,
|
|
|
|
|
ProfileId,
|
2024-02-22 11:18:13 -07:00
|
|
|
};
|
2023-12-22 14:31:01 -07:00
|
|
|
mod config;
|
2024-01-30 08:59:19 +01:00
|
|
|
mod mouse_reporter;
|
2023-12-22 14:31:01 -07:00
|
|
|
|
2024-01-11 13:20:16 -07:00
|
|
|
use icon_cache::IconCache;
|
|
|
|
|
mod icon_cache;
|
|
|
|
|
|
2024-03-18 19:31:28 -07:00
|
|
|
use key_bind::key_binds;
|
2024-01-19 10:45:35 -07:00
|
|
|
mod key_bind;
|
|
|
|
|
|
2026-07-09 14:18:02 +02:00
|
|
|
use layout::{LayoutNode, LayoutTab};
|
|
|
|
|
mod layout;
|
|
|
|
|
|
2025-12-29 20:42:52 -08:00
|
|
|
mod shortcuts;
|
|
|
|
|
|
2023-12-22 14:31:01 -07:00
|
|
|
mod localize;
|
|
|
|
|
|
2024-01-09 10:16:32 -07:00
|
|
|
use menu::menu_bar;
|
2023-12-22 15:00:50 -07:00
|
|
|
mod menu;
|
|
|
|
|
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
use terminal::{
|
|
|
|
|
AttentionKind, MONITOR_SILENCE_SECONDS, MonitorKind, PtyEvent, Terminal, TerminalPaneGrid,
|
|
|
|
|
TerminalScroll,
|
|
|
|
|
};
|
2023-12-20 13:31:10 -07:00
|
|
|
mod terminal;
|
2023-12-18 13:54:08 -07:00
|
|
|
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
mod desktop_notify;
|
|
|
|
|
|
|
|
|
|
use osc_tap::OscEvent;
|
|
|
|
|
mod osc_tap;
|
|
|
|
|
|
yoda: protocole clavier kitty, Shift+Entree pour Claude Code et Modjo
Claude Code (>= 2.1.269) n'active Shift+Entree que si le terminal repond
a CSI ? u. alacritty_terminal sait tenir la pile des modes et repondre,
mais seulement avec kitty_keyboard, et n'encode aucune touche : Shift+
Entree partait en \r, identique a Entree.
- term::Config : kitty_keyboard active.
- kitty_keyboard : encodeur du frontal (spec kitty, decoupe de l'encodeur
d'Alacritty), utilise seulement quand l'application au premier plan a
pousse le drapeau 1 ou 8 ; sinon l'encodage legacy reste seul. Gere
desambiguisation, types d'evenements (repetition, relachement),
touches alternatives, toutes les touches, texte associe, pave
numerique, F1-F24, touches de modification. Shift+Entree -> CSI 13;2u.
- terminal_box : raccourcis de l'appli toujours prioritaires ;
Shift+PageUp/PageDown/Home/End et Echap sur selection gardent leur
comportement local ; relachements envoyes si le drapeau 2 est pose.
- Legacy : Shift+Entree envoie LF (comme Ctrl+J) au lieu de CR. rustyline
le lit comme Ctrl+J, la touche de saut de ligne de Modjo/Goose ; pour
un shell c'est toujours Entree.
Connu, non corrige ici : alacritty_terminal 0.25.1 push_keyboard_mode vide
title_stack au lieu de la pile des modes au-dela de 4096 empilements
(panique du fil PTY de l'onglet seulement, panic=unwind).
10 tests, dont la reponse a CSI ? u et l'application de CSI > 5 u.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:32:23 +02:00
|
|
|
mod kitty_keyboard;
|
|
|
|
|
|
2024-01-09 10:16:32 -07:00
|
|
|
use terminal_box::terminal_box;
|
2024-08-03 20:33:55 -06:00
|
|
|
|
|
|
|
|
use crate::dnd::DndDrop;
|
2025-10-16 09:27:00 +02:00
|
|
|
use crate::menu::MenuState;
|
2023-12-20 13:31:10 -07:00
|
|
|
mod terminal_box;
|
2023-12-17 11:53:26 -07:00
|
|
|
|
2024-02-09 09:35:24 +01:00
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
mod password_manager;
|
2023-12-21 09:44:44 -07:00
|
|
|
mod terminal_theme;
|
|
|
|
|
|
2024-08-03 20:33:55 -06:00
|
|
|
mod dnd;
|
|
|
|
|
|
2025-06-23 19:22:32 -04:00
|
|
|
use clap_lex::RawArgs;
|
|
|
|
|
|
2025-07-13 15:50:37 +03:00
|
|
|
static ICON_CACHE: LazyLock<Mutex<IconCache>> = LazyLock::new(|| Mutex::new(IconCache::new()));
|
2024-01-11 13:20:16 -07:00
|
|
|
|
|
|
|
|
pub fn icon_cache_get(name: &'static str, size: u16) -> widget::icon::Icon {
|
|
|
|
|
let mut icon_cache = ICON_CACHE.lock().unwrap();
|
|
|
|
|
icon_cache.get(name, size)
|
|
|
|
|
}
|
|
|
|
|
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
/// Tab bar indicator for the monitor state of a terminal:
|
|
|
|
|
/// an alert icon if one fired, an "armed" icon while monitoring, else none.
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
/// Un appel d'attention (cloche, notification) passe avant les moniteurs.
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
fn monitor_icon_name(terminal: &Terminal) -> Option<&'static str> {
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
match terminal.attention {
|
|
|
|
|
Some(AttentionKind::Bell) => return Some("notification-alert-symbolic"),
|
|
|
|
|
Some(AttentionKind::Notification) => return Some("notification-new-symbolic"),
|
|
|
|
|
None => {}
|
|
|
|
|
}
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
match terminal.monitor_alert {
|
|
|
|
|
Some(MonitorKind::Activity) => Some("dialog-information-symbolic"),
|
|
|
|
|
Some(MonitorKind::Silence) => Some("alarm-symbolic"),
|
|
|
|
|
Some(MonitorKind::ProcessFinished) => Some("emblem-ok-symbolic"),
|
yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)
Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.
- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
plus la vue en bas. Couvre touches legacy et kitty, collage, clic
milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
application attendrait une reponse qui ne vient pas (CSI ? u de Claude
Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
(selection, copie et defilement locaux, meme dans une application qui
la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
action ToggleReadOnly dans les raccourcis (groupe Autre, sans
raccourci par defaut, comme Ghostty et Konsole) ; cadenas
changes-prevent-symbolic sur l'onglet, apres les alertes.
Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:19:02 +02:00
|
|
|
None if terminal.read_only => Some("changes-prevent-symbolic"),
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
None if terminal.monitoring_enabled() => Some("view-reveal-symbolic"),
|
|
|
|
|
None => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn monitor_update_tab_icon(
|
|
|
|
|
tab_model: &mut segmented_button::Model<segmented_button::SingleSelect>,
|
|
|
|
|
entity: segmented_button::Entity,
|
|
|
|
|
icon_name: Option<&'static str>,
|
|
|
|
|
) {
|
|
|
|
|
match icon_name {
|
|
|
|
|
Some(name) => {
|
|
|
|
|
tab_model.icon_set(entity, icon_cache_get(name, 16));
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
tab_model.icon_remove(entity);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 14:18:02 +02:00
|
|
|
/// 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()
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
/// Runs application with these settings
|
2025-06-23 19:22:32 -04:00
|
|
|
#[rustfmt::skip]
|
|
|
|
|
fn main() -> Result<(), Box<dyn Error>> {
|
|
|
|
|
let raw_args = RawArgs::from_args();
|
|
|
|
|
let mut cursor = raw_args.cursor();
|
2025-04-11 00:07:55 +02:00
|
|
|
|
2025-06-23 19:22:32 -04:00
|
|
|
let mut shell_program_opt = None;
|
|
|
|
|
let mut shell_args = Vec::new();
|
2024-03-14 18:04:31 +01:00
|
|
|
let mut daemonize = true;
|
2025-12-24 21:07:26 +00:00
|
|
|
let mut working_directory = None;
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
let mut profile_name_opt = None;
|
|
|
|
|
let mut list_profiles = false;
|
2025-06-23 19:22:32 -04:00
|
|
|
// Parse the arguments using clap_lex
|
|
|
|
|
while let Some(arg) = raw_args.next_os(&mut cursor) {
|
|
|
|
|
match arg.to_str() {
|
|
|
|
|
Some("--help") | Some("-h") => {
|
|
|
|
|
print_help();
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
Some("--version") | Some("-V") => {
|
|
|
|
|
println!(
|
2025-09-11 16:25:29 +02:00
|
|
|
"cosmic-term {}",
|
2025-06-23 19:22:32 -04:00
|
|
|
env!("CARGO_PKG_VERSION"),
|
|
|
|
|
);
|
|
|
|
|
return Ok(());
|
2025-04-08 17:03:44 +02:00
|
|
|
}
|
2026-01-15 20:27:52 +00:00
|
|
|
Some(arg_str @ "--working-directory") | Some(arg_str @ "-w") => {
|
2025-12-24 21:07:26 +00:00
|
|
|
if let Some(dir_arg) = raw_args.next_os(&mut cursor) {
|
|
|
|
|
working_directory = Some(PathBuf::from(dir_arg));
|
|
|
|
|
} else {
|
2026-01-15 20:27:52 +00:00
|
|
|
eprintln!("Missing argument for {arg_str}");
|
2025-12-24 21:07:26 +00:00
|
|
|
process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
Some(arg_str @ "--profile") | Some(arg_str @ "-p") => {
|
|
|
|
|
if let Some(profile_arg) = raw_args.next_os(&mut cursor) {
|
|
|
|
|
profile_name_opt = Some(profile_arg.to_string_lossy().to_string());
|
|
|
|
|
} else {
|
|
|
|
|
eprintln!("Missing argument for {arg_str}");
|
|
|
|
|
process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Some("--list-profiles") => {
|
|
|
|
|
list_profiles = true;
|
|
|
|
|
}
|
2025-06-23 19:22:32 -04:00
|
|
|
Some("--no-daemon") => {
|
2025-04-08 17:03:44 +02:00
|
|
|
daemonize = false;
|
|
|
|
|
}
|
2025-08-07 23:44:53 -04:00
|
|
|
Some("-e") | Some("--command") | Some("--") => {
|
2025-06-23 19:22:32 -04:00
|
|
|
// Handle the '--command' or '-e' flag
|
|
|
|
|
break;
|
|
|
|
|
}
|
2025-04-08 17:03:44 +02:00
|
|
|
_ => {
|
|
|
|
|
//TODO: should this throw an error?
|
|
|
|
|
log::warn!("ignored argument {:?}", arg);
|
2024-03-14 18:04:31 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-06-23 19:22:32 -04:00
|
|
|
// After flags, process remaining shell program and args
|
|
|
|
|
while let Some(arg) = raw_args.next_os(&mut cursor) {
|
2025-08-07 23:44:53 -04:00
|
|
|
if shell_program_opt.is_some() {
|
2025-06-23 19:22:32 -04:00
|
|
|
shell_args.push(arg.to_string_lossy().to_string());
|
|
|
|
|
} else {
|
|
|
|
|
shell_program_opt = Some(arg.to_string_lossy().to_string());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
// Load config before daemonizing so profile CLI options can print
|
|
|
|
|
// errors and listings to the launching terminal
|
2023-12-22 14:31:01 -07:00
|
|
|
let (config_handler, config) = match cosmic_config::Config::new(App::APP_ID, CONFIG_VERSION) {
|
|
|
|
|
Ok(config_handler) => {
|
|
|
|
|
let config = match Config::get_entry(&config_handler) {
|
|
|
|
|
Ok(ok) => ok,
|
|
|
|
|
Err((errs, config)) => {
|
|
|
|
|
log::info!("errors loading config: {:?}", errs);
|
|
|
|
|
config
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
(Some(config_handler), config)
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::error!("failed to create config handler: {}", err);
|
|
|
|
|
(None, Config::default())
|
|
|
|
|
}
|
|
|
|
|
};
|
2023-12-17 11:53:26 -07:00
|
|
|
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
if list_profiles {
|
|
|
|
|
for (name, profile_id) in config.profile_names() {
|
|
|
|
|
println!("{}\t{}", profile_id.0, name);
|
|
|
|
|
}
|
|
|
|
|
return Ok(());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let startup_profile_id = match &profile_name_opt {
|
|
|
|
|
Some(profile_name) => {
|
|
|
|
|
let profile_id_opt = config
|
|
|
|
|
.profile_names()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.find(|(name, _)| name == profile_name)
|
|
|
|
|
.map(|(_, profile_id)| profile_id)
|
|
|
|
|
.or_else(|| {
|
|
|
|
|
// Fall back to matching a numeric profile ID
|
|
|
|
|
let id = profile_name.parse::<u64>().ok()?;
|
|
|
|
|
config.profiles.contains_key(&ProfileId(id)).then_some(ProfileId(id))
|
|
|
|
|
});
|
|
|
|
|
if profile_id_opt.is_none() {
|
|
|
|
|
eprintln!("Profile {profile_name:?} not found. Available profiles:");
|
|
|
|
|
for (name, profile_id) in config.profile_names() {
|
|
|
|
|
eprintln!("{}\t{}", profile_id.0, name);
|
|
|
|
|
}
|
|
|
|
|
process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
profile_id_opt
|
|
|
|
|
}
|
|
|
|
|
None => None,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Platform-specific daemonization logic
|
|
|
|
|
|
|
|
|
|
#[cfg(all(unix, not(target_os = "redox")))]
|
|
|
|
|
if daemonize {
|
|
|
|
|
match fork::daemon(true, true) {
|
|
|
|
|
Ok(fork::Fork::Child) => (),
|
|
|
|
|
Ok(fork::Fork::Parent(_child_pid)) => process::exit(0),
|
|
|
|
|
Err(err) => {
|
|
|
|
|
eprintln!("failed to daemonize: {:?}", err);
|
|
|
|
|
process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
localize::localize();
|
|
|
|
|
|
2026-02-05 12:57:56 -07:00
|
|
|
let shortcuts_config = shortcuts::ShortcutsConfig::new(config.shortcuts_custom.clone());
|
2025-12-29 20:42:52 -08:00
|
|
|
|
2026-04-10 18:09:25 +02:00
|
|
|
let shell = shell_program_opt.map(|shell_program| tty::Shell::new(shell_program, shell_args));
|
2025-12-24 21:07:26 +00:00
|
|
|
let startup_options = Some(tty::Options {
|
|
|
|
|
shell,
|
|
|
|
|
working_directory,
|
|
|
|
|
..tty::Options::default()
|
|
|
|
|
});
|
2024-01-14 12:33:28 -07:00
|
|
|
|
2025-06-23 19:22:32 -04:00
|
|
|
// Terminal config setup
|
2026-05-29 11:02:10 -07:00
|
|
|
let term_config = term::Config {
|
|
|
|
|
scrolling_history: 100_000,
|
yoda: protocole clavier kitty, Shift+Entree pour Claude Code et Modjo
Claude Code (>= 2.1.269) n'active Shift+Entree que si le terminal repond
a CSI ? u. alacritty_terminal sait tenir la pile des modes et repondre,
mais seulement avec kitty_keyboard, et n'encode aucune touche : Shift+
Entree partait en \r, identique a Entree.
- term::Config : kitty_keyboard active.
- kitty_keyboard : encodeur du frontal (spec kitty, decoupe de l'encodeur
d'Alacritty), utilise seulement quand l'application au premier plan a
pousse le drapeau 1 ou 8 ; sinon l'encodage legacy reste seul. Gere
desambiguisation, types d'evenements (repetition, relachement),
touches alternatives, toutes les touches, texte associe, pave
numerique, F1-F24, touches de modification. Shift+Entree -> CSI 13;2u.
- terminal_box : raccourcis de l'appli toujours prioritaires ;
Shift+PageUp/PageDown/Home/End et Echap sur selection gardent leur
comportement local ; relachements envoyes si le drapeau 2 est pose.
- Legacy : Shift+Entree envoie LF (comme Ctrl+J) au lieu de CR. rustyline
le lit comme Ctrl+J, la touche de saut de ligne de Modjo/Goose ; pour
un shell c'est toujours Entree.
Connu, non corrige ici : alacritty_terminal 0.25.1 push_keyboard_mode vide
title_stack au lieu de la pile des modes au-dela de 4096 empilements
(panique du fil PTY de l'onglet seulement, panic=unwind).
10 tests, dont la reponse a CSI ? u et l'application de CSI > 5 u.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:32:23 +02:00
|
|
|
// [yoda] Protocole clavier kitty : alacritty_terminal tient la pile des
|
|
|
|
|
// modes et répond à `CSI ? u`, l'encodage est dans `kitty_keyboard`.
|
|
|
|
|
kitty_keyboard: true,
|
2026-05-29 11:02:10 -07:00
|
|
|
..term::Config::default()
|
|
|
|
|
};
|
2023-12-21 10:11:11 -07:00
|
|
|
// Set up environmental variables for terminal
|
2023-12-31 12:50:54 +03:00
|
|
|
tty::setup_env();
|
2023-12-21 10:14:57 -07:00
|
|
|
// Override TERM for better compatibility
|
2025-09-10 14:33:46 +02:00
|
|
|
unsafe {
|
|
|
|
|
env::set_var("TERM", "xterm-256color");
|
|
|
|
|
}
|
2023-12-17 11:53:26 -07:00
|
|
|
|
2025-06-23 19:22:32 -04:00
|
|
|
// Set settings
|
2023-12-22 14:31:01 -07:00
|
|
|
let mut settings = Settings::default();
|
|
|
|
|
settings = settings.theme(config.app_theme.theme());
|
2024-01-31 10:52:56 -07:00
|
|
|
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
|
2023-12-22 14:31:01 -07:00
|
|
|
|
2025-06-23 19:22:32 -04:00
|
|
|
// Flags
|
2023-12-22 14:31:01 -07:00
|
|
|
let flags = Flags {
|
|
|
|
|
config_handler,
|
|
|
|
|
config,
|
2025-12-29 20:42:52 -08:00
|
|
|
shortcuts_config,
|
2024-01-14 12:33:28 -07:00
|
|
|
startup_options,
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
startup_profile_id,
|
2023-12-22 14:31:01 -07:00
|
|
|
term_config,
|
|
|
|
|
};
|
2025-08-07 23:44:53 -04:00
|
|
|
|
2025-06-23 19:22:32 -04:00
|
|
|
// Run the cosmic app
|
2023-12-22 14:31:01 -07:00
|
|
|
cosmic::app::run::<App>(settings, flags)?;
|
2023-12-20 13:31:10 -07:00
|
|
|
|
|
|
|
|
Ok(())
|
2023-12-17 22:51:50 -07:00
|
|
|
}
|
|
|
|
|
|
2025-06-23 19:22:32 -04:00
|
|
|
fn print_help() {
|
|
|
|
|
println!(
|
|
|
|
|
r#"COSMIC Terminal
|
|
|
|
|
Designed for the COSMIC™ desktop environment, cosmic-term is a libcosmic-based terminal emulator.
|
2025-08-07 23:44:53 -04:00
|
|
|
|
2025-06-23 19:22:32 -04:00
|
|
|
Project home page: https://github.com/pop-os/cosmic-term
|
|
|
|
|
Options:
|
2025-12-24 21:07:26 +00:00
|
|
|
--help Show this message
|
|
|
|
|
--version Show the version of cosmic-term
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
-w, --working-directory <dir> Set the working directory for the terminal
|
|
|
|
|
-p, --profile <name-or-id> Open the first tab with the given profile
|
|
|
|
|
--list-profiles List profiles (ID and name) and exit
|
|
|
|
|
--no-daemon Do not detach from the launching terminal
|
|
|
|
|
-e, --command <command...> Run a command instead of the default shell;
|
|
|
|
|
everything after this flag (or after `--`)
|
|
|
|
|
is treated as the command and its arguments"#
|
2025-06-23 19:22:32 -04:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-22 14:31:01 -07:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub struct Flags {
|
|
|
|
|
config_handler: Option<cosmic_config::Config>,
|
|
|
|
|
config: Config,
|
2025-12-29 20:42:52 -08:00
|
|
|
shortcuts_config: shortcuts::ShortcutsConfig,
|
2024-01-14 12:33:28 -07:00
|
|
|
startup_options: Option<tty::Options>,
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
startup_profile_id: Option<ProfileId>,
|
2024-03-13 09:12:07 -06:00
|
|
|
term_config: term::Config,
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
|
2024-01-19 11:03:14 -07:00
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
2023-12-22 15:00:50 -07:00
|
|
|
pub enum Action {
|
2024-02-27 21:20:00 -07:00
|
|
|
About,
|
2024-09-15 23:53:13 +02:00
|
|
|
ClearScrollback,
|
2024-02-22 11:18:13 -07:00
|
|
|
ColorSchemes(ColorSchemeKind),
|
2023-12-22 15:00:50 -07:00
|
|
|
Copy,
|
2026-02-15 10:23:49 +01:00
|
|
|
CopyUrlByMenu,
|
2024-05-03 14:37:01 -07:00
|
|
|
CopyOrSigint,
|
2024-01-21 21:13:13 +01:00
|
|
|
CopyPrimary,
|
2024-01-19 10:45:35 -07:00
|
|
|
Find,
|
2026-02-05 11:31:05 -07:00
|
|
|
KeyboardShortcuts,
|
2025-10-16 09:27:00 +02:00
|
|
|
LaunchUrlByMenu,
|
2026-07-09 14:18:02 +02:00
|
|
|
LayoutLoad,
|
|
|
|
|
LayoutSave,
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
Monitor(MonitorKind),
|
yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)
Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.
- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
plus la vue en bas. Couvre touches legacy et kitty, collage, clic
milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
application attendrait une reponse qui ne vient pas (CSI ? u de Claude
Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
(selection, copie et defilement locaux, meme dans une application qui
la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
action ToggleReadOnly dans les raccourcis (groupe Autre, sans
raccourci par defaut, comme Ghostty et Konsole) ; cadenas
changes-prevent-symbolic sur l'onglet, apres les alertes.
Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:19:02 +02:00
|
|
|
ToggleReadOnly,
|
2024-01-19 10:45:35 -07:00
|
|
|
PaneFocusDown,
|
|
|
|
|
PaneFocusLeft,
|
|
|
|
|
PaneFocusRight,
|
|
|
|
|
PaneFocusUp,
|
|
|
|
|
PaneSplitHorizontal,
|
|
|
|
|
PaneSplitVertical,
|
|
|
|
|
PaneToggleMaximized,
|
2023-12-22 15:00:50 -07:00
|
|
|
Paste,
|
2024-01-20 14:21:51 +01:00
|
|
|
PastePrimary,
|
2024-02-09 15:45:46 -07:00
|
|
|
ProfileOpen(ProfileId),
|
|
|
|
|
Profiles,
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
SaveOutput,
|
2023-12-22 15:00:50 -07:00
|
|
|
SelectAll,
|
2023-12-22 15:16:40 -07:00
|
|
|
Settings,
|
2026-07-09 14:18:02 +02:00
|
|
|
SshConnect(usize),
|
2024-02-09 09:35:24 +01:00
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
PasswordManager,
|
2023-12-22 15:40:10 -07:00
|
|
|
ShowHeaderBar(bool),
|
2024-01-19 10:45:35 -07:00
|
|
|
TabActivate0,
|
|
|
|
|
TabActivate1,
|
|
|
|
|
TabActivate2,
|
|
|
|
|
TabActivate3,
|
|
|
|
|
TabActivate4,
|
|
|
|
|
TabActivate5,
|
|
|
|
|
TabActivate6,
|
|
|
|
|
TabActivate7,
|
|
|
|
|
TabActivate8,
|
|
|
|
|
TabClose,
|
2023-12-22 15:16:40 -07:00
|
|
|
TabNew,
|
2024-08-13 02:20:52 -04:00
|
|
|
TabNewNoProfile,
|
2024-01-19 10:45:35 -07:00
|
|
|
TabNext,
|
|
|
|
|
TabPrev,
|
2026-05-24 10:27:32 +02:00
|
|
|
TabRename,
|
2025-06-19 06:38:57 +02:00
|
|
|
ToggleFullscreen,
|
2024-01-19 11:03:14 -07:00
|
|
|
WindowClose,
|
|
|
|
|
WindowNew,
|
2024-01-19 10:45:35 -07:00
|
|
|
ZoomIn,
|
|
|
|
|
ZoomOut,
|
|
|
|
|
ZoomReset,
|
2023-12-22 15:00:50 -07:00
|
|
|
}
|
|
|
|
|
|
2024-05-20 14:12:21 -06:00
|
|
|
impl Action {
|
2024-03-18 19:31:28 -07:00
|
|
|
fn message(&self, entity_opt: Option<segmented_button::Entity>) -> Message {
|
2023-12-22 15:00:50 -07:00
|
|
|
match self {
|
2024-04-15 21:05:30 +01:00
|
|
|
Self::About => Message::ToggleContextPage(ContextPage::About),
|
2024-09-15 23:53:13 +02:00
|
|
|
Self::ClearScrollback => Message::ClearScrollback(entity_opt),
|
2024-04-15 21:05:30 +01:00
|
|
|
Self::ColorSchemes(color_scheme_kind) => {
|
2024-03-18 19:31:28 -07:00
|
|
|
Message::ToggleContextPage(ContextPage::ColorSchemes(*color_scheme_kind))
|
2024-02-22 11:18:13 -07:00
|
|
|
}
|
2024-04-15 21:05:30 +01:00
|
|
|
Self::Copy => Message::Copy(entity_opt),
|
2026-02-15 10:23:49 +01:00
|
|
|
Self::CopyUrlByMenu => Message::CopyUrlByMenu,
|
2024-05-03 14:37:01 -07:00
|
|
|
Self::CopyOrSigint => Message::CopyOrSigint(entity_opt),
|
2024-04-26 10:27:52 -06:00
|
|
|
Self::CopyPrimary => Message::CopyPrimary(entity_opt),
|
2024-04-15 21:05:30 +01:00
|
|
|
Self::Find => Message::Find(true),
|
2026-02-05 11:31:05 -07:00
|
|
|
Self::KeyboardShortcuts => Message::ToggleContextPage(ContextPage::KeyboardShortcuts),
|
2025-10-16 09:27:00 +02:00
|
|
|
Self::LaunchUrlByMenu => Message::LaunchUrlByMenu,
|
2026-07-09 14:18:02 +02:00
|
|
|
Self::LayoutLoad => Message::LayoutLoad,
|
|
|
|
|
Self::LayoutSave => Message::LayoutSave,
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
Self::Monitor(monitor_kind) => Message::MonitorToggle(*monitor_kind, entity_opt),
|
yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)
Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.
- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
plus la vue en bas. Couvre touches legacy et kitty, collage, clic
milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
application attendrait une reponse qui ne vient pas (CSI ? u de Claude
Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
(selection, copie et defilement locaux, meme dans une application qui
la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
action ToggleReadOnly dans les raccourcis (groupe Autre, sans
raccourci par defaut, comme Ghostty et Konsole) ; cadenas
changes-prevent-symbolic sur l'onglet, apres les alertes.
Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:19:02 +02:00
|
|
|
Self::ToggleReadOnly => Message::ReadOnlyToggle(entity_opt),
|
2024-04-15 21:05:30 +01:00
|
|
|
Self::PaneFocusDown => Message::PaneFocusAdjacent(pane_grid::Direction::Down),
|
|
|
|
|
Self::PaneFocusLeft => Message::PaneFocusAdjacent(pane_grid::Direction::Left),
|
|
|
|
|
Self::PaneFocusRight => Message::PaneFocusAdjacent(pane_grid::Direction::Right),
|
|
|
|
|
Self::PaneFocusUp => Message::PaneFocusAdjacent(pane_grid::Direction::Up),
|
|
|
|
|
Self::PaneSplitHorizontal => Message::PaneSplit(pane_grid::Axis::Horizontal),
|
|
|
|
|
Self::PaneSplitVertical => Message::PaneSplit(pane_grid::Axis::Vertical),
|
|
|
|
|
Self::PaneToggleMaximized => Message::PaneToggleMaximized,
|
2024-02-09 09:35:24 +01:00
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
Self::PasswordManager => Message::ToggleContextPage(ContextPage::PasswordManager),
|
2024-04-15 21:05:30 +01:00
|
|
|
Self::Paste => Message::Paste(entity_opt),
|
2024-04-26 10:27:52 -06:00
|
|
|
Self::PastePrimary => Message::PastePrimary(entity_opt),
|
2024-04-15 21:05:30 +01:00
|
|
|
Self::ProfileOpen(profile_id) => Message::ProfileOpen(*profile_id),
|
|
|
|
|
Self::Profiles => Message::ToggleContextPage(ContextPage::Profiles),
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
Self::SaveOutput => Message::SaveOutput(entity_opt),
|
2024-04-15 21:05:30 +01:00
|
|
|
Self::SelectAll => Message::SelectAll(entity_opt),
|
|
|
|
|
Self::Settings => Message::ToggleContextPage(ContextPage::Settings),
|
2026-07-09 14:18:02 +02:00
|
|
|
Self::SshConnect(index) => Message::SshConnect(*index),
|
2024-04-15 21:05:30 +01:00
|
|
|
Self::ShowHeaderBar(show_headerbar) => Message::ShowHeaderBar(*show_headerbar),
|
|
|
|
|
Self::TabActivate0 => Message::TabActivateJump(0),
|
|
|
|
|
Self::TabActivate1 => Message::TabActivateJump(1),
|
|
|
|
|
Self::TabActivate2 => Message::TabActivateJump(2),
|
|
|
|
|
Self::TabActivate3 => Message::TabActivateJump(3),
|
|
|
|
|
Self::TabActivate4 => Message::TabActivateJump(4),
|
|
|
|
|
Self::TabActivate5 => Message::TabActivateJump(5),
|
|
|
|
|
Self::TabActivate6 => Message::TabActivateJump(6),
|
|
|
|
|
Self::TabActivate7 => Message::TabActivateJump(7),
|
|
|
|
|
Self::TabActivate8 => Message::TabActivateJump(8),
|
|
|
|
|
Self::TabClose => Message::TabClose(entity_opt),
|
|
|
|
|
Self::TabNew => Message::TabNew,
|
2024-08-13 02:20:52 -04:00
|
|
|
Self::TabNewNoProfile => Message::TabNewNoProfile,
|
2024-04-15 21:05:30 +01:00
|
|
|
Self::TabNext => Message::TabNext,
|
|
|
|
|
Self::TabPrev => Message::TabPrev,
|
2026-05-24 10:27:32 +02:00
|
|
|
Self::TabRename => Message::TabRenameOpen(entity_opt),
|
2025-06-19 06:38:57 +02:00
|
|
|
Self::ToggleFullscreen => Message::ToggleFullscreen,
|
2024-04-15 21:05:30 +01:00
|
|
|
Self::WindowClose => Message::WindowClose,
|
|
|
|
|
Self::WindowNew => Message::WindowNew,
|
|
|
|
|
Self::ZoomIn => Message::ZoomIn,
|
|
|
|
|
Self::ZoomOut => Message::ZoomOut,
|
|
|
|
|
Self::ZoomReset => Message::ZoomReset,
|
2023-12-22 15:00:50 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-20 14:12:21 -06:00
|
|
|
impl MenuAction for Action {
|
|
|
|
|
type Message = Message;
|
|
|
|
|
|
|
|
|
|
fn message(&self) -> Message {
|
|
|
|
|
self.message(None)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
/// Messages that are used specifically by our [`App`].
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub enum Message {
|
2023-12-22 14:31:01 -07:00
|
|
|
AppTheme(AppTheme),
|
2024-09-15 23:53:13 +02:00
|
|
|
ClearScrollback(Option<segmented_button::Entity>),
|
2024-02-22 11:18:13 -07:00
|
|
|
ColorSchemeCollapse,
|
|
|
|
|
ColorSchemeDelete(ColorSchemeKind, ColorSchemeId),
|
2024-06-05 08:51:26 -06:00
|
|
|
ColorSchemeExpand(ColorSchemeKind, Option<ColorSchemeId>),
|
|
|
|
|
ColorSchemeExport(ColorSchemeKind, Option<ColorSchemeId>),
|
|
|
|
|
ColorSchemeExportResult(ColorSchemeKind, Option<ColorSchemeId>, DialogResult),
|
2024-02-22 11:18:13 -07:00
|
|
|
ColorSchemeImport(ColorSchemeKind),
|
|
|
|
|
ColorSchemeImportResult(ColorSchemeKind, DialogResult),
|
2024-02-28 09:15:58 -07:00
|
|
|
ColorSchemeRename(ColorSchemeKind, ColorSchemeId, String),
|
|
|
|
|
ColorSchemeRenameSubmit,
|
2024-02-22 11:18:13 -07:00
|
|
|
ColorSchemeTabActivate(widget::segmented_button::Entity),
|
2026-04-10 19:00:25 +02:00
|
|
|
Config(Box<Config>),
|
2023-12-22 15:00:50 -07:00
|
|
|
Copy(Option<segmented_button::Entity>),
|
2024-05-03 14:37:01 -07:00
|
|
|
CopyOrSigint(Option<segmented_button::Entity>),
|
2024-01-21 21:13:13 +01:00
|
|
|
CopyPrimary(Option<segmented_button::Entity>),
|
2026-02-15 10:23:49 +01:00
|
|
|
CopyUrlByMenu,
|
2024-02-28 09:15:58 -07:00
|
|
|
DefaultBoldFontWeight(usize),
|
|
|
|
|
DefaultDimFontWeight(usize),
|
2023-12-22 14:31:01 -07:00
|
|
|
DefaultFont(usize),
|
|
|
|
|
DefaultFontSize(usize),
|
2024-01-10 01:19:04 +03:00
|
|
|
DefaultFontStretch(usize),
|
|
|
|
|
DefaultFontWeight(usize),
|
2024-01-01 12:27:51 +03:00
|
|
|
DefaultZoomStep(usize),
|
2026-04-10 18:32:09 +02:00
|
|
|
DialogMessage(Box<DialogMessage>), // DialogMessage is huge, so we use a box to make the size of this enum smaller on the stack
|
2024-08-03 20:33:55 -06:00
|
|
|
Drop(Option<(pane_grid::Pane, segmented_button::Entity, DndDrop)>),
|
2024-01-11 13:20:16 -07:00
|
|
|
Find(bool),
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
FindCaseSensitive(bool),
|
2024-01-11 13:20:16 -07:00
|
|
|
FindNext,
|
|
|
|
|
FindPrevious,
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
FindRegex(bool),
|
2024-01-11 13:20:16 -07:00
|
|
|
FindSearchValueChanged(String),
|
2024-01-21 21:13:13 +01:00
|
|
|
MiddleClick(pane_grid::Pane, Option<segmented_button::Entity>),
|
2024-02-28 09:15:58 -07:00
|
|
|
FocusFollowMouse(bool),
|
2026-05-26 15:40:59 -04:00
|
|
|
Key(Modifiers, Physical, Key),
|
2024-02-28 09:15:58 -07:00
|
|
|
LaunchUrl(String),
|
2025-10-16 09:27:00 +02:00
|
|
|
LaunchUrlByMenu,
|
2026-07-09 14:18:02 +02:00
|
|
|
LayoutLoad,
|
|
|
|
|
LayoutLoadApply,
|
|
|
|
|
LayoutLoadCancel,
|
|
|
|
|
LayoutLoadResult(DialogResult),
|
|
|
|
|
LayoutSave,
|
|
|
|
|
LayoutSaveResult(DialogResult),
|
2024-01-11 13:20:16 -07:00
|
|
|
Modifiers(Modifiers),
|
2025-12-29 20:42:52 -08:00
|
|
|
ShortcutCaptureCancel,
|
|
|
|
|
ShortcutCaptureStart(shortcuts::KeyBindAction),
|
2026-02-03 22:08:34 -08:00
|
|
|
ShortcutConflictCancel,
|
|
|
|
|
ShortcutConflictReplace,
|
2025-12-29 20:42:52 -08:00
|
|
|
ShortcutRemove(shortcuts::Binding, shortcuts::BindingSource),
|
2026-02-05 12:57:56 -07:00
|
|
|
ShortcutReset(shortcuts::KeyBindAction),
|
2026-02-05 11:25:53 -07:00
|
|
|
ShortcutSearch(String),
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
MonitorTick,
|
|
|
|
|
MonitorToggle(MonitorKind, Option<segmented_button::Entity>),
|
yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)
Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.
- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
plus la vue en bas. Couvre touches legacy et kitty, collage, clic
milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
application attendrait une reponse qui ne vient pas (CSI ? u de Claude
Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
(selection, copie et defilement locaux, meme dans une application qui
la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
action ToggleReadOnly dans les raccourcis (groupe Autre, sans
raccourci par defaut, comme Ghostty et Konsole) ; cadenas
changes-prevent-symbolic sur l'onglet, apres les alertes.
Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:19:02 +02:00
|
|
|
ReadOnlyToggle(Option<segmented_button::Entity>),
|
2024-01-22 16:35:23 +01:00
|
|
|
MouseEnter(pane_grid::Pane),
|
2024-02-12 12:36:30 -07:00
|
|
|
Opacity(u8),
|
2024-02-28 09:15:58 -07:00
|
|
|
PaneClicked(pane_grid::Pane),
|
|
|
|
|
PaneDragged(pane_grid::DragEvent),
|
|
|
|
|
PaneFocusAdjacent(pane_grid::Direction),
|
|
|
|
|
PaneResized(pane_grid::ResizeEvent),
|
|
|
|
|
PaneSplit(pane_grid::Axis),
|
|
|
|
|
PaneToggleMaximized,
|
2024-02-09 09:35:24 +01:00
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
PasswordManager(password_manager::PasswordManagerMessage),
|
|
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
PasswordPaste(secstr::SecUtf8, pane_grid::Pane),
|
2023-12-22 15:00:50 -07:00
|
|
|
Paste(Option<segmented_button::Entity>),
|
2024-01-20 14:13:04 +01:00
|
|
|
PastePrimary(Option<segmented_button::Entity>),
|
2023-12-22 15:00:50 -07:00
|
|
|
PasteValue(Option<segmented_button::Entity>, String),
|
2024-02-09 15:45:46 -07:00
|
|
|
ProfileCollapse(ProfileId),
|
|
|
|
|
ProfileCommand(ProfileId, String),
|
2024-04-14 21:17:32 +02:00
|
|
|
ProfileDirectory(ProfileId, String),
|
2024-02-09 15:45:46 -07:00
|
|
|
ProfileExpand(ProfileId),
|
2024-04-14 21:17:32 +02:00
|
|
|
ProfileHold(ProfileId, bool),
|
2024-02-09 15:45:46 -07:00
|
|
|
ProfileName(ProfileId, String),
|
|
|
|
|
ProfileNew,
|
|
|
|
|
ProfileOpen(ProfileId),
|
|
|
|
|
ProfileRemove(ProfileId),
|
2024-02-22 11:18:13 -07:00
|
|
|
ProfileSyntaxTheme(ProfileId, ColorSchemeKind, usize),
|
2024-02-09 15:45:46 -07:00
|
|
|
ProfileTabTitle(ProfileId, String),
|
2026-01-28 20:08:44 -05:00
|
|
|
ReorderTab(Pane, ReorderEvent),
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
SaveOutput(Option<segmented_button::Entity>),
|
|
|
|
|
SaveOutputResult(pane_grid::Pane, segmented_button::Entity, DialogResult),
|
2026-09-08 13:49:17 -06:00
|
|
|
Surface(surface::Action<Message>),
|
2023-12-22 15:00:50 -07:00
|
|
|
SelectAll(Option<segmented_button::Entity>),
|
2024-02-28 09:15:58 -07:00
|
|
|
ShowAdvancedFontSettings(bool),
|
2023-12-22 15:40:10 -07:00
|
|
|
ShowHeaderBar(bool),
|
2026-07-09 14:18:02 +02:00
|
|
|
SshConnect(usize),
|
2026-04-06 18:38:10 -06:00
|
|
|
ShowPaneBorders(bool),
|
yoda: attenuation des panneaux non focalises, a la Ghostty
Avec Claude Code et Modjo cote a cote, rien ne distinguait le panneau ou
l'on tape, hors le curseur creux. L'amont assombrissait le fond de 8 %
(aussi quand la fenetre perdait le focus) et l'a retire en juillet
(a43b5ae).
Reprise de unfocused-split-opacity de Ghostty : un voile de la couleur du
fond, texte compris, sur chaque panneau non focalise, seulement quand il y
en a plusieurs ; la perte de focus de la fenetre ne change rien.
- Config unfocused_pane_dim (pourcentage, defaut 30 = opacite 0,7 de
Ghostty, 0 desactive, plafond 85 = opacite minimale 0,15 de Ghostty).
- Reglages > Panneaux : curseur « Attenuation des panneaux inactifs ».
- terminal_box : voile trace dans un calque a part en fin de rendu (dans
le meme calque, la primitive de texte passerait par-dessus), aux coins
du fond du panneau.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:40:47 +02:00
|
|
|
UnfocusedPaneDim(u8),
|
2026-08-19 22:02:40 +02:00
|
|
|
|
2024-02-22 11:18:13 -07:00
|
|
|
SyntaxTheme(ColorSchemeKind, usize),
|
2024-01-31 15:18:20 -07:00
|
|
|
SystemThemeChange,
|
2026-03-06 13:53:46 +01:00
|
|
|
TabNewInheritWorkingDirectory(bool),
|
2026-10-01 14:38:17 +02:00
|
|
|
CopyOnSelectClipboard(bool),
|
|
|
|
|
SelectionDone(pane_grid::Pane, segmented_button::Entity),
|
2023-12-20 13:31:10 -07:00
|
|
|
TabActivate(segmented_button::Entity),
|
2024-01-18 02:44:25 -05:00
|
|
|
TabActivateJump(usize),
|
2024-01-09 10:16:32 -07:00
|
|
|
TabClose(Option<segmented_button::Entity>),
|
2023-12-22 15:00:50 -07:00
|
|
|
TabContextAction(segmented_button::Entity, Action),
|
2025-10-17 07:56:43 +02:00
|
|
|
TabContextMenu(pane_grid::Pane, Option<MenuState>),
|
2023-12-20 13:31:10 -07:00
|
|
|
TabNew,
|
2024-08-13 02:20:52 -04:00
|
|
|
TabNewNoProfile,
|
2024-01-17 22:58:04 -05:00
|
|
|
TabNext,
|
2024-02-28 09:15:58 -07:00
|
|
|
TabPrev,
|
2026-05-24 10:27:32 +02:00
|
|
|
TabRenameOpenForPane(pane_grid::Pane, segmented_button::Entity),
|
|
|
|
|
TabRenameOpen(Option<segmented_button::Entity>),
|
|
|
|
|
TabRenameInput(String),
|
|
|
|
|
TabRenameCommit,
|
|
|
|
|
TabRenameCancel,
|
|
|
|
|
TabRenameResetToDefault,
|
2024-01-17 16:09:56 +01:00
|
|
|
TermEvent(pane_grid::Pane, segmented_button::Entity, TermEvent),
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
TermEventTx(mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, PtyEvent)>),
|
|
|
|
|
TermOsc(pane_grid::Pane, segmented_button::Entity, OscEvent),
|
|
|
|
|
DesktopNotify(desktop_notify::Event),
|
2025-06-19 06:38:57 +02:00
|
|
|
ToggleFullscreen,
|
2023-12-22 14:31:01 -07:00
|
|
|
ToggleContextPage(ContextPage),
|
2024-02-28 09:15:58 -07:00
|
|
|
UpdateDefaultProfile((bool, ProfileId)),
|
|
|
|
|
UseBrightBold(bool),
|
2024-01-09 10:16:32 -07:00
|
|
|
WindowClose,
|
|
|
|
|
WindowNew,
|
2025-01-21 17:07:45 +01:00
|
|
|
WindowFocused,
|
|
|
|
|
WindowUnfocused,
|
2024-01-09 10:16:32 -07:00
|
|
|
ZoomIn,
|
|
|
|
|
ZoomOut,
|
|
|
|
|
ZoomReset,
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
|
|
|
pub enum ContextPage {
|
2024-02-27 21:20:00 -07:00
|
|
|
About,
|
2024-02-22 11:18:13 -07:00
|
|
|
ColorSchemes(ColorSchemeKind),
|
2026-02-05 10:34:58 -07:00
|
|
|
KeyboardShortcuts,
|
2024-02-09 15:45:46 -07:00
|
|
|
Profiles,
|
2023-12-22 14:31:01 -07:00
|
|
|
Settings,
|
2024-02-09 09:35:24 +01:00
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
PasswordManager,
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
|
2026-02-03 22:08:34 -08:00
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
struct ShortcutConflict {
|
|
|
|
|
binding: shortcuts::Binding,
|
|
|
|
|
existing_action: shortcuts::KeyBindAction,
|
|
|
|
|
new_action: shortcuts::KeyBindAction,
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
/// The [`App`] stores application-specific state.
|
|
|
|
|
pub struct App {
|
|
|
|
|
core: Core,
|
2025-09-11 16:25:29 +02:00
|
|
|
about: About,
|
2024-01-17 16:09:56 +01:00
|
|
|
pane_model: TerminalPaneGrid,
|
2023-12-22 14:31:01 -07:00
|
|
|
config_handler: Option<cosmic_config::Config>,
|
|
|
|
|
config: Config,
|
2025-12-29 20:42:52 -08:00
|
|
|
shortcuts_config: shortcuts::ShortcutsConfig,
|
2024-01-19 10:45:35 -07:00
|
|
|
key_binds: HashMap<KeyBind, Action>,
|
2023-12-22 14:31:01 -07:00
|
|
|
app_themes: Vec<String>,
|
|
|
|
|
font_names: Vec<String>,
|
|
|
|
|
font_size_names: Vec<String>,
|
|
|
|
|
font_sizes: Vec<u16>,
|
2024-01-10 01:19:04 +03:00
|
|
|
font_name_faces_map: BTreeMap<String, Vec<FaceInfo>>,
|
|
|
|
|
all_font_weights_vals_names_map: BTreeMap<u16, String>,
|
|
|
|
|
all_font_stretches_vals_names_map: BTreeMap<Stretch, String>,
|
|
|
|
|
curr_font_weight_names: Vec<String>,
|
|
|
|
|
curr_font_weights: Vec<u16>,
|
|
|
|
|
curr_font_stretch_names: Vec<String>,
|
|
|
|
|
curr_font_stretches: Vec<Stretch>,
|
2024-01-01 12:27:51 +03:00
|
|
|
zoom_step_names: Vec<String>,
|
|
|
|
|
zoom_steps: Vec<u16>,
|
2024-02-22 11:18:13 -07:00
|
|
|
theme_names_dark: Vec<String>,
|
|
|
|
|
theme_names_light: Vec<String>,
|
|
|
|
|
themes: HashMap<(String, ColorSchemeKind), TermColors>,
|
2023-12-22 14:31:01 -07:00
|
|
|
context_page: ContextPage,
|
2024-02-22 09:55:46 -07:00
|
|
|
dialog_opt: Option<Dialog<Message>>,
|
2024-01-17 16:09:56 +01:00
|
|
|
terminal_ids: HashMap<pane_grid::Pane, widget::Id>,
|
2024-01-11 13:20:16 -07:00
|
|
|
find: bool,
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
find_case_sensitive: bool,
|
|
|
|
|
find_regex: bool,
|
2024-01-11 13:20:16 -07:00
|
|
|
find_search_id: widget::Id,
|
|
|
|
|
find_search_value: String,
|
2026-05-18 16:30:51 +02:00
|
|
|
term_event_tx_opt:
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
Option<mpsc::UnboundedSender<(pane_grid::Pane, segmented_button::Entity, PtyEvent)>>,
|
|
|
|
|
notify_tx_opt: Option<mpsc::UnboundedSender<desktop_notify::Request>>,
|
|
|
|
|
window_focused: bool,
|
2024-01-14 12:33:28 -07:00
|
|
|
startup_options: Option<tty::Options>,
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
startup_profile_id: Option<ProfileId>,
|
2024-03-13 09:12:07 -06:00
|
|
|
term_config: term::Config,
|
2024-02-22 09:55:46 -07:00
|
|
|
color_scheme_errors: Vec<String>,
|
2024-06-05 08:51:26 -06:00
|
|
|
color_scheme_expanded: Option<(ColorSchemeKind, Option<ColorSchemeId>)>,
|
2024-02-22 11:18:13 -07:00
|
|
|
color_scheme_renaming: Option<(ColorSchemeKind, ColorSchemeId, String)>,
|
2024-02-22 09:55:46 -07:00
|
|
|
color_scheme_rename_id: widget::Id,
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
rename_tab: Option<(pane_grid::Pane, segmented_button::Entity, String)>,
|
2026-05-24 10:27:32 +02:00
|
|
|
rename_tab_id: widget::Id,
|
2026-07-09 14:18:02 +02:00
|
|
|
layout_load_pending: Option<LayoutNode>,
|
|
|
|
|
ssh_hosts: Vec<String>,
|
2024-02-22 11:18:13 -07:00
|
|
|
color_scheme_tab_model: widget::segmented_button::SingleSelectModel,
|
2024-02-09 15:45:46 -07:00
|
|
|
profile_expanded: Option<ProfileId>,
|
2024-01-10 01:38:13 +03:00
|
|
|
show_advanced_font_settings: bool,
|
2025-12-29 20:42:52 -08:00
|
|
|
shortcut_capture: Option<shortcuts::KeyBindAction>,
|
2026-02-03 22:08:34 -08:00
|
|
|
shortcut_conflict: Option<ShortcutConflict>,
|
|
|
|
|
shortcut_conflict_overlay_restore: Option<bool>,
|
2026-02-05 11:25:53 -07:00
|
|
|
shortcut_search_focus: Cell<bool>,
|
|
|
|
|
shortcut_search_id: widget::Id,
|
|
|
|
|
shortcut_search_regex: Option<regex::Regex>,
|
|
|
|
|
shortcut_search_value: String,
|
2024-01-11 13:20:16 -07:00
|
|
|
modifiers: Modifiers,
|
2024-02-09 09:35:24 +01:00
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
password_mgr: password_manager::PasswordManager,
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
|
2023-12-22 14:31:01 -07:00
|
|
|
impl App {
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
/// L'onglet est-il sous les yeux de l'utilisateur : onglet actif du panneau
|
|
|
|
|
/// focalisé, dans une fenêtre qui a le focus.
|
|
|
|
|
fn tab_in_view(&self, pane: pane_grid::Pane, entity: segmented_button::Entity) -> bool {
|
|
|
|
|
self.window_focused
|
|
|
|
|
&& self.pane_model.focused() == pane
|
|
|
|
|
&& self
|
|
|
|
|
.pane_model
|
|
|
|
|
.panes
|
|
|
|
|
.get(pane)
|
|
|
|
|
.is_some_and(|tab_model| tab_model.active() == entity)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Allume l'indicateur d'attention d'un onglet ; il s'éteint quand
|
|
|
|
|
/// l'onglet reprend le focus (voir `TerminalPaneGrid::update_terminal_focus`).
|
|
|
|
|
fn set_tab_attention(
|
|
|
|
|
&mut self,
|
|
|
|
|
pane: pane_grid::Pane,
|
|
|
|
|
entity: segmented_button::Entity,
|
|
|
|
|
kind: AttentionKind,
|
|
|
|
|
) {
|
|
|
|
|
let Some(tab_model) = self.pane_model.panes.get_mut(pane) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let mut icon_update = None;
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
// Une notification n'est pas rétrogradée en simple cloche
|
|
|
|
|
if terminal.attention != Some(AttentionKind::Notification) {
|
|
|
|
|
terminal.attention = Some(kind);
|
|
|
|
|
icon_update = Some(monitor_icon_name(&terminal));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(icon_name) = icon_update {
|
|
|
|
|
monitor_update_tab_icon(tab_model, entity, icon_name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Recale le focus des terminaux après un changement d'onglet : l'onglet
|
|
|
|
|
/// quitté reçoit son focus-out (mode 1004), le nouvel onglet son focus-in.
|
|
|
|
|
fn sync_terminal_focus(&mut self) {
|
|
|
|
|
if self.window_focused && !self.core.window.show_context && self.rename_tab.is_none() {
|
|
|
|
|
self.pane_model.update_terminal_focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-22 11:18:13 -07:00
|
|
|
fn theme_names(&self, color_scheme_kind: ColorSchemeKind) -> &Vec<String> {
|
|
|
|
|
match color_scheme_kind {
|
|
|
|
|
ColorSchemeKind::Dark => &self.theme_names_dark,
|
|
|
|
|
ColorSchemeKind::Light => &self.theme_names_light,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-22 09:55:46 -07:00
|
|
|
fn update_color_schemes(&mut self) {
|
|
|
|
|
self.themes = terminal_theme::terminal_themes();
|
2024-02-22 11:18:13 -07:00
|
|
|
for &color_scheme_kind in &[ColorSchemeKind::Dark, ColorSchemeKind::Light] {
|
|
|
|
|
for (color_scheme_name, color_scheme_id) in
|
|
|
|
|
self.config.color_scheme_names(color_scheme_kind)
|
|
|
|
|
{
|
|
|
|
|
if let Some(color_scheme) = self
|
|
|
|
|
.config
|
|
|
|
|
.color_schemes(color_scheme_kind)
|
|
|
|
|
.get(&color_scheme_id)
|
2026-04-10 18:09:25 +02:00
|
|
|
&& self
|
2024-02-22 11:18:13 -07:00
|
|
|
.themes
|
|
|
|
|
.insert(
|
|
|
|
|
(color_scheme_name.clone(), color_scheme_kind),
|
|
|
|
|
color_scheme.into(),
|
|
|
|
|
)
|
|
|
|
|
.is_some()
|
2026-04-10 18:32:09 +02:00
|
|
|
{
|
|
|
|
|
log::warn!(
|
|
|
|
|
"custom {:?} color scheme {:?} replaces builtin one",
|
|
|
|
|
color_scheme_kind,
|
|
|
|
|
color_scheme_name
|
|
|
|
|
);
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-22 11:18:13 -07:00
|
|
|
self.theme_names_dark.clear();
|
|
|
|
|
self.theme_names_light.clear();
|
|
|
|
|
for (name, color_scheme_kind) in self.themes.keys() {
|
|
|
|
|
match *color_scheme_kind {
|
|
|
|
|
ColorSchemeKind::Dark => {
|
|
|
|
|
self.theme_names_dark.push(name.clone());
|
|
|
|
|
}
|
|
|
|
|
ColorSchemeKind::Light => {
|
|
|
|
|
self.theme_names_light.push(name.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
2024-02-22 11:18:13 -07:00
|
|
|
self.theme_names_dark
|
2024-07-10 03:22:23 -04:00
|
|
|
.sort_by(|a, b| LANGUAGE_SORTER.compare(a, b));
|
2024-02-22 11:18:13 -07:00
|
|
|
self.theme_names_light
|
2024-07-10 03:22:23 -04:00
|
|
|
.sort_by(|a, b| LANGUAGE_SORTER.compare(a, b));
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
|
|
|
|
|
2024-08-20 04:52:34 +03:00
|
|
|
fn reset_terminal_panes_zoom(&mut self) {
|
|
|
|
|
for (_pane, 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.set_zoom_adj(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-27 20:33:44 +03:00
|
|
|
fn reset_active_pane_zoom(&mut self) {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
for entity in tab_model.iter() {
|
|
|
|
|
if tab_model.is_active(entity)
|
|
|
|
|
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
{
|
|
|
|
|
terminal.lock().unwrap().set_zoom_adj(0);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-29 20:42:52 -08:00
|
|
|
fn save_shortcuts_custom(&mut self) {
|
2026-01-29 20:10:25 -08:00
|
|
|
self.config.shortcuts_custom = self.shortcuts_config.custom.clone();
|
|
|
|
|
match &self.config_handler {
|
2025-12-29 20:42:52 -08:00
|
|
|
Some(config_handler) => {
|
2026-02-05 10:34:58 -07:00
|
|
|
if let Err(err) =
|
|
|
|
|
config_handler.set("shortcuts_custom", &self.config.shortcuts_custom)
|
|
|
|
|
{
|
2025-12-29 20:42:52 -08:00
|
|
|
log::warn!("failed to save shortcuts custom config: {}", err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
log::warn!("failed to save shortcuts custom config: no config handler");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.key_binds = key_binds(&self.shortcuts_config);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 22:08:34 -08:00
|
|
|
fn apply_shortcut_binding(
|
|
|
|
|
&mut self,
|
|
|
|
|
binding: shortcuts::Binding,
|
|
|
|
|
action: shortcuts::KeyBindAction,
|
|
|
|
|
) {
|
|
|
|
|
self.shortcuts_config.custom.0.insert(binding, action);
|
|
|
|
|
self.save_shortcuts_custom();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_context_overlay(&mut self, overlay: bool) {
|
|
|
|
|
if self.core.window.context_is_overlay != overlay {
|
|
|
|
|
self.core.window.context_is_overlay = overlay;
|
|
|
|
|
self.core.set_show_context(self.core.window.show_context);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn begin_shortcut_conflict(&mut self, conflict: ShortcutConflict) {
|
|
|
|
|
if self.shortcut_conflict.is_none() {
|
|
|
|
|
self.shortcut_conflict_overlay_restore = Some(self.core.window.context_is_overlay);
|
|
|
|
|
self.set_context_overlay(false);
|
|
|
|
|
}
|
|
|
|
|
self.shortcut_conflict = Some(conflict);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn clear_shortcut_conflict(&mut self) {
|
|
|
|
|
self.shortcut_conflict = None;
|
|
|
|
|
if let Some(overlay) = self.shortcut_conflict_overlay_restore.take() {
|
|
|
|
|
self.set_context_overlay(overlay);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 11:25:53 -07:00
|
|
|
fn shortcut_page_toggle(&mut self) {
|
|
|
|
|
self.shortcut_capture = None;
|
|
|
|
|
self.clear_shortcut_conflict();
|
|
|
|
|
self.shortcut_search_focus
|
|
|
|
|
.set(self.core.window.show_context);
|
|
|
|
|
self.shortcut_search_regex = None;
|
|
|
|
|
self.shortcut_search_value.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-22 12:31:02 -06:00
|
|
|
fn update_config(&mut self) -> Task<Message> {
|
2024-02-12 13:04:39 -07:00
|
|
|
let theme = self.config.app_theme.theme();
|
|
|
|
|
|
2024-02-22 09:55:46 -07:00
|
|
|
// Update color schemes
|
|
|
|
|
self.update_color_schemes();
|
|
|
|
|
|
2024-02-12 13:04:39 -07:00
|
|
|
// Update terminal window background color
|
|
|
|
|
{
|
2026-07-06 09:26:53 -04:00
|
|
|
let color = Color::from(theme.cosmic().background(theme.transparent).base);
|
2024-02-12 13:04:39 -07:00
|
|
|
let bytes = color.into_rgba8();
|
2024-04-15 20:44:25 +01:00
|
|
|
let data = u32::from(bytes[2])
|
|
|
|
|
| (u32::from(bytes[1]) << 8)
|
|
|
|
|
| (u32::from(bytes[0]) << 16)
|
2024-02-12 13:04:39 -07:00
|
|
|
| 0xFF000000;
|
|
|
|
|
terminal::WINDOW_BG_COLOR.store(data, Ordering::SeqCst);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set config of all tabs
|
2026-06-12 13:47:00 -07:00
|
|
|
let color_scheme_kind = self.config.color_scheme_kind(&theme);
|
2024-02-12 13:04:39 -07:00
|
|
|
for (_pane, tab_model) in self.pane_model.panes.iter() {
|
|
|
|
|
for entity in tab_model.iter() {
|
2024-01-17 16:09:56 +01:00
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
2026-06-12 13:47:00 -07:00
|
|
|
terminal.set_config(&self.config, color_scheme_kind, &self.themes);
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-22 15:40:10 -07:00
|
|
|
|
2024-02-12 13:04:39 -07:00
|
|
|
// Set headerbar state
|
2023-12-22 15:40:10 -07:00
|
|
|
self.core.window.show_headerbar = self.config.show_headerbar;
|
2024-02-12 13:04:39 -07:00
|
|
|
|
|
|
|
|
// Update application theme
|
2025-03-19 12:27:00 -04:00
|
|
|
cosmic::command::set_theme(theme)
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-22 12:31:02 -06:00
|
|
|
fn update_render_active_pane_zoom(&mut self, zoom_message: Message) -> Task<Message> {
|
2024-08-20 04:52:34 +03:00
|
|
|
// skip writing config to fs when zoom in/ out
|
|
|
|
|
// recalculate the pane due to the changes of zoom_adj value
|
|
|
|
|
// but only for the active pane/tab
|
2026-06-12 13:47:00 -07:00
|
|
|
let color_scheme_kind = self.config.color_scheme_kind(self.core.system_theme());
|
2024-08-20 04:52:34 +03:00
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
for entity in tab_model.iter() {
|
2026-04-10 18:09:25 +02:00
|
|
|
if tab_model.is_active(entity)
|
2026-04-10 18:32:09 +02:00
|
|
|
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
{
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
let current_zoom_adj = terminal.zoom_adj();
|
|
|
|
|
match zoom_message {
|
|
|
|
|
Message::ZoomIn => {
|
|
|
|
|
terminal.set_zoom_adj(current_zoom_adj.saturating_add(1))
|
|
|
|
|
}
|
|
|
|
|
Message::ZoomOut => {
|
|
|
|
|
terminal.set_zoom_adj(current_zoom_adj.saturating_sub(1))
|
2024-08-20 04:52:34 +03:00
|
|
|
}
|
2026-04-10 18:32:09 +02:00
|
|
|
_ => {}
|
2024-08-20 04:52:34 +03:00
|
|
|
}
|
2026-06-12 13:47:00 -07:00
|
|
|
terminal.set_config(&self.config, color_scheme_kind, &self.themes);
|
2024-08-20 04:52:34 +03:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-22 12:31:02 -06:00
|
|
|
Task::none()
|
2024-08-20 04:52:34 +03:00
|
|
|
}
|
|
|
|
|
|
2024-10-22 12:31:02 -06:00
|
|
|
fn save_color_schemes(&mut self, color_scheme_kind: ColorSchemeKind) -> Task<Message> {
|
2024-02-22 09:55:46 -07:00
|
|
|
// Optimized for just saving color_schemes
|
2026-04-10 18:09:25 +02:00
|
|
|
if let Some(ref config_handler) = self.config_handler
|
|
|
|
|
&& let Err(err) = config_handler.set(
|
2024-02-22 11:18:13 -07:00
|
|
|
match color_scheme_kind {
|
|
|
|
|
ColorSchemeKind::Dark => "color_schemes_dark",
|
|
|
|
|
ColorSchemeKind::Light => "color_schemes_light",
|
|
|
|
|
},
|
2024-04-15 20:52:07 +01:00
|
|
|
self.config.color_schemes(color_scheme_kind),
|
2026-04-10 18:32:09 +02:00
|
|
|
)
|
|
|
|
|
{
|
|
|
|
|
log::error!("failed to save config: {}", err);
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
|
|
|
|
self.update_color_schemes();
|
2024-10-22 12:31:02 -06:00
|
|
|
Task::none()
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-22 12:31:02 -06:00
|
|
|
fn save_profiles(&mut self) -> Task<Message> {
|
2024-02-09 15:45:46 -07:00
|
|
|
// Optimized for just saving profiles
|
|
|
|
|
if let Some(ref config_handler) = self.config_handler {
|
|
|
|
|
match config_handler.set("profiles", &self.config.profiles) {
|
|
|
|
|
Ok(()) => {}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::error!("failed to save config: {}", err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-10-22 12:31:02 -06:00
|
|
|
Task::none()
|
2024-02-09 15:45:46 -07:00
|
|
|
}
|
|
|
|
|
|
2024-10-22 12:31:02 -06:00
|
|
|
fn update_focus(&self) -> Task<Message> {
|
2026-05-24 10:27:32 +02:00
|
|
|
if self.rename_tab.is_some() {
|
|
|
|
|
widget::text_input::focus(self.rename_tab_id.clone())
|
|
|
|
|
} else if self.find {
|
2024-01-11 13:20:16 -07:00
|
|
|
widget::text_input::focus(self.find_search_id.clone())
|
2025-10-24 17:56:10 -04:00
|
|
|
} else if self.core.window.show_context {
|
2026-04-10 18:36:16 +02:00
|
|
|
// Right now we only care about the KeyboardShortcuts context page, so we use a simple if.
|
|
|
|
|
// In the future if we are to care about other conext pages, we could switch this to a match
|
|
|
|
|
// statement instead to be cleaner.
|
|
|
|
|
if self.context_page == ContextPage::KeyboardShortcuts
|
|
|
|
|
&& self.shortcut_search_focus.get()
|
|
|
|
|
{
|
|
|
|
|
self.shortcut_search_focus.set(false);
|
|
|
|
|
return widget::text_input::focus(self.shortcut_search_id.clone());
|
2026-02-05 11:25:53 -07:00
|
|
|
}
|
2026-04-10 18:36:16 +02:00
|
|
|
|
2025-10-24 17:56:10 -04:00
|
|
|
Task::none()
|
2025-01-21 17:07:45 +01:00
|
|
|
} else if let Some(terminal_id) = self.terminal_ids.get(&self.pane_model.focused()).cloned()
|
|
|
|
|
{
|
2024-01-17 16:09:56 +01:00
|
|
|
widget::text_input::focus(terminal_id)
|
2024-01-11 13:20:16 -07:00
|
|
|
} else {
|
2024-10-22 12:31:02 -06:00
|
|
|
Task::none()
|
2024-01-11 13:20:16 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Call this any time the tab changes
|
2024-10-22 12:31:02 -06:00
|
|
|
fn update_title(&mut self, pane: Option<pane_grid::Pane>) -> Task<Message> {
|
2025-01-21 17:07:45 +01:00
|
|
|
let pane = pane.unwrap_or(self.pane_model.focused());
|
2024-01-17 16:09:56 +01:00
|
|
|
if let Some(tab_model) = self.pane_model.panes.get(pane) {
|
|
|
|
|
let (header_title, window_title) = match tab_model.text(tab_model.active()) {
|
|
|
|
|
Some(tab_title) => (
|
|
|
|
|
tab_title.to_string(),
|
2024-02-27 21:20:00 -07:00
|
|
|
format!("{tab_title} — {}", fl!("cosmic-terminal")),
|
2024-01-17 16:09:56 +01:00
|
|
|
),
|
2024-02-27 21:20:00 -07:00
|
|
|
None => (String::new(), fl!("cosmic-terminal")),
|
2024-01-17 16:09:56 +01:00
|
|
|
};
|
|
|
|
|
self.set_header_title(header_title);
|
2024-10-22 12:31:02 -06:00
|
|
|
Task::batch([
|
|
|
|
|
if let Some(window_id) = self.core.main_window_id() {
|
|
|
|
|
self.set_window_title(window_title, window_id)
|
|
|
|
|
} else {
|
|
|
|
|
Task::none()
|
|
|
|
|
},
|
2024-02-22 09:55:46 -07:00
|
|
|
self.update_focus(),
|
|
|
|
|
])
|
2024-01-17 16:09:56 +01:00
|
|
|
} else {
|
|
|
|
|
log::error!("Failed to get the specific pane");
|
2024-10-22 12:31:02 -06:00
|
|
|
Task::batch([
|
|
|
|
|
if let Some(window_id) = self.core.main_window_id() {
|
|
|
|
|
self.set_window_title(fl!("cosmic-terminal"), window_id)
|
|
|
|
|
} else {
|
|
|
|
|
Task::none()
|
|
|
|
|
},
|
2024-01-17 16:09:56 +01:00
|
|
|
self.update_focus(),
|
|
|
|
|
])
|
|
|
|
|
}
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
|
2024-01-10 01:19:04 +03:00
|
|
|
fn set_curr_font_weights_and_stretches(&mut self) {
|
2024-01-02 18:23:44 +03:00
|
|
|
// check if config font_name is available first, if not, set it to first name in list
|
|
|
|
|
if !self.font_names.contains(&self.config.font_name) {
|
|
|
|
|
log::error!("'{}' is not in the font list", self.config.font_name);
|
|
|
|
|
log::error!("setting font name to '{}'", self.font_names[0]);
|
2024-01-12 21:45:57 +03:00
|
|
|
let _ = self.update(Message::DefaultFont(0));
|
2024-01-02 18:23:44 +03:00
|
|
|
}
|
|
|
|
|
|
2024-01-10 01:19:04 +03:00
|
|
|
let curr_font_faces = &self.font_name_faces_map[&self.config.font_name];
|
|
|
|
|
|
|
|
|
|
self.curr_font_stretches = curr_font_faces
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|face| face.stretch)
|
|
|
|
|
.collect::<BTreeSet<_>>() // remove duplicates and sort
|
|
|
|
|
.into_iter()
|
|
|
|
|
.collect();
|
|
|
|
|
|
2024-01-11 11:33:39 -07:00
|
|
|
self.curr_font_stretch_names = self
|
|
|
|
|
.curr_font_stretches
|
|
|
|
|
.iter()
|
2024-01-10 01:19:04 +03:00
|
|
|
.map(|stretch| &self.all_font_stretches_vals_names_map[stretch])
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
2024-01-11 11:33:39 -07:00
|
|
|
if !self
|
|
|
|
|
.curr_font_stretches
|
|
|
|
|
.contains(&self.config.typed_font_stretch())
|
|
|
|
|
{
|
2024-01-10 01:19:04 +03:00
|
|
|
self.config.font_stretch = Stretch::Normal.to_number();
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-11 11:33:39 -07:00
|
|
|
let curr_weights = |conf_stretch| {
|
|
|
|
|
curr_font_faces
|
|
|
|
|
.iter()
|
|
|
|
|
.filter(|face| face.stretch == conf_stretch)
|
|
|
|
|
.map(|face| face.weight.0)
|
|
|
|
|
.collect::<BTreeSet<_>>() // remove duplicates and sort
|
|
|
|
|
.into_iter()
|
|
|
|
|
.collect()
|
|
|
|
|
};
|
2024-01-10 01:19:04 +03:00
|
|
|
|
|
|
|
|
self.curr_font_weights = curr_weights(self.config.typed_font_stretch());
|
|
|
|
|
|
|
|
|
|
if self.curr_font_weights.is_empty() {
|
|
|
|
|
// stretch fallback
|
2024-01-11 11:33:39 -07:00
|
|
|
self.config.font_stretch = Stretch::Normal.to_number();
|
2024-01-10 01:19:04 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.curr_font_weights = curr_weights(self.config.typed_font_stretch());
|
|
|
|
|
assert!(!self.curr_font_weights.is_empty());
|
|
|
|
|
|
2024-01-11 11:33:39 -07:00
|
|
|
self.curr_font_weight_names = self
|
|
|
|
|
.curr_font_weights
|
|
|
|
|
.iter()
|
2024-01-10 01:19:04 +03:00
|
|
|
.map(|weight| &self.all_font_weights_vals_names_map[weight])
|
|
|
|
|
.cloned()
|
|
|
|
|
.collect::<Vec<_>>();
|
|
|
|
|
|
|
|
|
|
if !self.curr_font_weights.contains(&self.config.font_weight) {
|
|
|
|
|
self.config.font_weight = Weight::NORMAL.0;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-12 01:58:57 +03:00
|
|
|
if !self
|
|
|
|
|
.curr_font_weights
|
|
|
|
|
.contains(&self.config.dim_font_weight)
|
|
|
|
|
{
|
|
|
|
|
self.config.dim_font_weight = Weight::NORMAL.0;
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-11 11:33:39 -07:00
|
|
|
if !self
|
|
|
|
|
.curr_font_weights
|
|
|
|
|
.contains(&self.config.bold_font_weight)
|
|
|
|
|
{
|
2024-01-10 01:19:04 +03:00
|
|
|
self.config.bold_font_weight = Weight::BOLD.0;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 14:33:46 +02:00
|
|
|
fn color_schemes(&self, color_scheme_kind: ColorSchemeKind) -> Element<'_, Message> {
|
2024-02-22 09:55:46 -07:00
|
|
|
let cosmic_theme::Spacing { space_xxxs, .. } = self.core().system_theme().cosmic().spacing;
|
|
|
|
|
|
2024-02-22 11:18:13 -07:00
|
|
|
let mut sections = Vec::with_capacity(3 + self.color_scheme_errors.len());
|
2024-02-22 09:55:46 -07:00
|
|
|
|
2024-02-22 11:18:13 -07:00
|
|
|
sections.push(
|
2024-02-26 15:19:54 -07:00
|
|
|
widget::tab_bar::horizontal(&self.color_scheme_tab_model)
|
2024-02-22 11:18:13 -07:00
|
|
|
.on_activate(Message::ColorSchemeTabActivate)
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
|
2024-10-22 12:31:02 -06:00
|
|
|
let mut section = widget::settings::section();
|
2024-06-05 08:51:26 -06:00
|
|
|
let builtin_name = format!("COSMIC {:?}", color_scheme_kind);
|
|
|
|
|
let color_scheme_names = self.config.color_scheme_names(color_scheme_kind);
|
|
|
|
|
for (color_scheme_name, color_scheme_id_opt) in std::iter::once((builtin_name, None)).chain(
|
|
|
|
|
color_scheme_names
|
|
|
|
|
.into_iter()
|
|
|
|
|
.map(|(name, id)| (name, Some(id))),
|
|
|
|
|
) {
|
|
|
|
|
let expanded =
|
|
|
|
|
self.color_scheme_expanded == Some((color_scheme_kind, color_scheme_id_opt));
|
|
|
|
|
let renaming = match &self.color_scheme_renaming {
|
|
|
|
|
Some((kind, id, value))
|
|
|
|
|
if kind == &color_scheme_kind && Some(id) == color_scheme_id_opt.as_ref() =>
|
|
|
|
|
{
|
|
|
|
|
Some(value)
|
2024-02-27 10:33:24 -07:00
|
|
|
}
|
2024-06-05 08:51:26 -06:00
|
|
|
_ => None,
|
|
|
|
|
};
|
2024-02-22 09:55:46 -07:00
|
|
|
|
2024-06-05 08:51:26 -06:00
|
|
|
let button = if expanded {
|
2024-09-20 19:44:36 -06:00
|
|
|
widget::button::custom(icon_cache_get("view-more-symbolic", 16))
|
2024-06-05 08:51:26 -06:00
|
|
|
.on_press(Message::ColorSchemeCollapse)
|
|
|
|
|
} else {
|
2024-09-20 19:44:36 -06:00
|
|
|
widget::button::custom(icon_cache_get("view-more-symbolic", 16)).on_press(
|
2024-06-05 08:51:26 -06:00
|
|
|
Message::ColorSchemeExpand(color_scheme_kind, color_scheme_id_opt),
|
|
|
|
|
)
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
2024-10-22 12:31:02 -06:00
|
|
|
.class(style::Button::Icon);
|
2024-06-05 08:51:26 -06:00
|
|
|
|
|
|
|
|
let mut popover = widget::popover(button);
|
|
|
|
|
if expanded {
|
|
|
|
|
let menu = menu::color_scheme_menu(
|
|
|
|
|
color_scheme_kind,
|
|
|
|
|
color_scheme_id_opt,
|
|
|
|
|
&color_scheme_name,
|
|
|
|
|
);
|
|
|
|
|
popover = popover
|
|
|
|
|
.popup(menu)
|
|
|
|
|
.position(widget::popover::Position::Bottom);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let item = match renaming {
|
|
|
|
|
Some(value) => widget::settings::item_row(vec![
|
|
|
|
|
widget::text_input("", value)
|
|
|
|
|
.id(self.color_scheme_rename_id.clone())
|
|
|
|
|
.on_input(move |value| {
|
|
|
|
|
Message::ColorSchemeRename(
|
|
|
|
|
color_scheme_kind,
|
|
|
|
|
color_scheme_id_opt.expect("trying to rename builtin color scheme"),
|
|
|
|
|
value,
|
|
|
|
|
)
|
|
|
|
|
})
|
2025-03-19 12:27:00 -04:00
|
|
|
.on_submit(|_| Message::ColorSchemeRenameSubmit)
|
2024-06-05 08:51:26 -06:00
|
|
|
.into(),
|
|
|
|
|
popover.into(),
|
|
|
|
|
]),
|
|
|
|
|
None => widget::settings::item::builder(color_scheme_name).control(popover),
|
|
|
|
|
};
|
|
|
|
|
section = section.add(item);
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
2024-06-05 08:51:26 -06:00
|
|
|
sections.push(section.into());
|
2024-02-22 09:55:46 -07:00
|
|
|
|
|
|
|
|
sections.push(
|
|
|
|
|
widget::row::with_children(vec![
|
2026-03-18 12:21:57 -04:00
|
|
|
widget::space::horizontal().into(),
|
2024-02-22 09:55:46 -07:00
|
|
|
widget::button::standard(fl!("import"))
|
2024-02-22 11:18:13 -07:00
|
|
|
.on_press(Message::ColorSchemeImport(color_scheme_kind))
|
2024-02-22 09:55:46 -07:00
|
|
|
.into(),
|
|
|
|
|
])
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
|
2024-04-14 21:09:14 +01:00
|
|
|
for error in &self.color_scheme_errors {
|
2024-02-22 09:55:46 -07:00
|
|
|
sections.push(
|
|
|
|
|
widget::row::with_children(vec![
|
|
|
|
|
icon_cache_get("dialog-error-symbolic", 16)
|
2024-10-22 12:31:02 -06:00
|
|
|
.class(style::Svg::Custom(Rc::new(|theme| {
|
2024-02-22 09:55:46 -07:00
|
|
|
let cosmic = theme.cosmic();
|
2024-10-22 12:31:02 -06:00
|
|
|
widget::svg::Style {
|
2024-02-22 09:55:46 -07:00
|
|
|
color: Some(cosmic.destructive_text_color().into()),
|
|
|
|
|
}
|
2024-10-22 12:31:02 -06:00
|
|
|
})))
|
2024-02-22 09:55:46 -07:00
|
|
|
.into(),
|
2024-10-22 12:31:02 -06:00
|
|
|
widget::text::body(error)
|
|
|
|
|
.class(style::Text::Custom(|theme| {
|
2024-02-22 09:55:46 -07:00
|
|
|
let cosmic = theme.cosmic();
|
|
|
|
|
//TODO: re-export in libcosmic
|
2024-10-22 12:31:02 -06:00
|
|
|
iced::widget::text::Style {
|
2024-02-22 09:55:46 -07:00
|
|
|
color: Some(cosmic.destructive_text_color().into()),
|
2026-05-26 15:40:59 -04:00
|
|
|
..Default::default()
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
|
|
|
|
}))
|
|
|
|
|
.into(),
|
|
|
|
|
])
|
|
|
|
|
.spacing(space_xxxs)
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
widget::settings::view_column(sections).into()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 10:34:58 -07:00
|
|
|
fn keyboard_shortcuts(&self) -> Element<'_, Message> {
|
|
|
|
|
let cosmic_theme::Spacing {
|
2026-02-05 13:17:18 -07:00
|
|
|
space_xxs,
|
|
|
|
|
space_s,
|
|
|
|
|
space_m,
|
|
|
|
|
space_l,
|
|
|
|
|
space_xl,
|
|
|
|
|
..
|
2026-02-05 10:34:58 -07:00
|
|
|
} = self.core().system_theme().cosmic().spacing;
|
|
|
|
|
|
2026-02-05 13:17:18 -07:00
|
|
|
let pad_action = [space_xxs, space_m];
|
|
|
|
|
let div_action = space_s;
|
|
|
|
|
let pad_binding = [space_xxs, space_xl];
|
|
|
|
|
let div_binding = space_l;
|
2026-02-05 10:34:58 -07:00
|
|
|
|
|
|
|
|
let mut groups = Vec::new();
|
2026-02-05 11:25:53 -07:00
|
|
|
//TODO: fix text input focus going outside bounds
|
2026-03-18 12:21:57 -04:00
|
|
|
groups.push(widget::space::horizontal().into());
|
2026-02-05 11:25:53 -07:00
|
|
|
groups.push(
|
|
|
|
|
widget::text_input::search_input(fl!("type-to-search"), &self.shortcut_search_value)
|
|
|
|
|
.id(self.shortcut_search_id.clone())
|
|
|
|
|
.on_input(Message::ShortcutSearch)
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
|
2026-02-05 10:34:58 -07:00
|
|
|
for group in shortcuts::shortcut_groups() {
|
|
|
|
|
let mut list = widget::list::list_column();
|
|
|
|
|
|
2026-02-05 11:25:53 -07:00
|
|
|
let mut found_actions = false;
|
2026-02-05 10:34:58 -07:00
|
|
|
for action in group.actions {
|
2026-02-05 11:25:53 -07:00
|
|
|
let action_label = shortcuts::action_label(action);
|
2026-04-10 18:09:25 +02:00
|
|
|
if let Some(regex) = &self.shortcut_search_regex
|
2026-04-10 18:32:09 +02:00
|
|
|
&& regex.find(&action_label).is_none()
|
|
|
|
|
{
|
|
|
|
|
continue;
|
2026-02-05 11:25:53 -07:00
|
|
|
}
|
|
|
|
|
found_actions = true;
|
|
|
|
|
|
2026-02-05 12:57:56 -07:00
|
|
|
let (bindings, changed) = self.shortcuts_config.bindings_for_action(action);
|
2026-02-05 10:34:58 -07:00
|
|
|
|
2026-02-05 12:57:56 -07:00
|
|
|
let mut buttons = widget::row::with_capacity(2);
|
|
|
|
|
if changed {
|
|
|
|
|
buttons = buttons.push(widget::tooltip(
|
|
|
|
|
widget::button::custom(icon_cache_get("edit-undo-symbolic", 16))
|
2026-02-05 10:34:58 -07:00
|
|
|
.class(style::Button::Icon)
|
2026-02-05 12:57:56 -07:00
|
|
|
.on_press(Message::ShortcutReset(action)),
|
|
|
|
|
widget::text::body(fl!("reset-to-default")),
|
|
|
|
|
widget::tooltip::Position::Top,
|
|
|
|
|
));
|
|
|
|
|
}
|
|
|
|
|
buttons = buttons.push(widget::tooltip(
|
|
|
|
|
widget::button::custom(icon_cache_get("list-add-symbolic", 16))
|
|
|
|
|
.class(style::Button::Icon)
|
|
|
|
|
.on_press(Message::ShortcutCaptureStart(action)),
|
|
|
|
|
widget::text::body(fl!("add-another-keybinding")),
|
|
|
|
|
widget::tooltip::Position::Top,
|
|
|
|
|
));
|
|
|
|
|
|
2026-02-05 13:17:18 -07:00
|
|
|
list = list.list_item_padding(pad_action);
|
|
|
|
|
list = list.divider_padding(div_action);
|
|
|
|
|
list = list.add(widget::settings::item_row(vec![
|
|
|
|
|
widget::text::heading(action_label)
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
.into(),
|
|
|
|
|
buttons.into(),
|
|
|
|
|
]));
|
2026-02-05 10:34:58 -07:00
|
|
|
|
|
|
|
|
if bindings.is_empty() {
|
2026-02-05 13:17:18 -07:00
|
|
|
list = list.list_item_padding(pad_binding);
|
2026-02-05 10:34:58 -07:00
|
|
|
list = list.add(widget::text::body(fl!("no-shortcuts")));
|
2026-02-05 13:17:18 -07:00
|
|
|
list = list.divider_padding(div_binding);
|
2026-02-05 10:34:58 -07:00
|
|
|
} else {
|
|
|
|
|
for resolved in bindings {
|
2026-02-05 13:17:18 -07:00
|
|
|
list = list.list_item_padding(pad_binding);
|
2026-02-05 10:34:58 -07:00
|
|
|
list = list.add(
|
|
|
|
|
widget::settings::item::builder(shortcuts::binding_display(
|
|
|
|
|
&resolved.binding,
|
|
|
|
|
))
|
|
|
|
|
.control(
|
|
|
|
|
widget::button::custom(icon_cache_get("edit-delete-symbolic", 16))
|
|
|
|
|
.class(style::Button::Icon)
|
|
|
|
|
.on_press(Message::ShortcutRemove(
|
|
|
|
|
resolved.binding.clone(),
|
|
|
|
|
resolved.source,
|
|
|
|
|
)),
|
|
|
|
|
),
|
|
|
|
|
);
|
2026-02-05 13:17:18 -07:00
|
|
|
list = list.divider_padding(div_binding);
|
2026-02-05 10:34:58 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.shortcut_capture == Some(action) {
|
2026-02-05 13:17:18 -07:00
|
|
|
list = list.list_item_padding(pad_binding);
|
2026-02-05 10:34:58 -07:00
|
|
|
list = list.add(
|
2026-02-05 13:36:38 -07:00
|
|
|
widget::settings::item_row(vec![
|
|
|
|
|
widget::text::body(fl!("shortcut-capture-hint"))
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
.into(),
|
|
|
|
|
widget::button::text(fl!("cancel"))
|
2026-02-05 10:34:58 -07:00
|
|
|
.on_press(Message::ShortcutCaptureCancel)
|
|
|
|
|
.into(),
|
|
|
|
|
])
|
|
|
|
|
.spacing(space_xxs),
|
|
|
|
|
);
|
2026-02-05 13:17:18 -07:00
|
|
|
list = list.divider_padding(div_binding);
|
2026-02-05 10:34:58 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 11:25:53 -07:00
|
|
|
if found_actions {
|
|
|
|
|
groups.push(
|
|
|
|
|
widget::settings::section::with_column(list)
|
|
|
|
|
.title(group.title)
|
|
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-02-05 10:34:58 -07:00
|
|
|
}
|
|
|
|
|
|
2026-02-05 11:25:53 -07:00
|
|
|
widget::settings::view_column(groups).into()
|
2026-02-05 10:34:58 -07:00
|
|
|
}
|
|
|
|
|
|
2025-09-10 14:33:46 +02:00
|
|
|
fn profiles(&self) -> Element<'_, Message> {
|
2024-02-09 15:45:46 -07:00
|
|
|
let cosmic_theme::Spacing {
|
|
|
|
|
space_s,
|
|
|
|
|
space_xs,
|
|
|
|
|
space_xxs,
|
|
|
|
|
space_xxxs,
|
|
|
|
|
..
|
|
|
|
|
} = self.core().system_theme().cosmic().spacing;
|
|
|
|
|
|
|
|
|
|
let mut sections = Vec::with_capacity(2);
|
|
|
|
|
|
|
|
|
|
if !self.config.profiles.is_empty() {
|
2024-10-22 12:31:02 -06:00
|
|
|
let mut profiles_section = widget::settings::section();
|
2024-02-09 15:45:46 -07:00
|
|
|
for (profile_name, profile_id) in self.config.profile_names() {
|
2024-04-15 20:47:12 +01:00
|
|
|
let Some(profile) = self.config.profiles.get(&profile_id) else {
|
|
|
|
|
continue;
|
2024-02-09 15:45:46 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let expanded = self.profile_expanded == Some(profile_id);
|
|
|
|
|
|
|
|
|
|
profiles_section = profiles_section.add(
|
|
|
|
|
widget::settings::item::builder(profile_name).control(
|
|
|
|
|
widget::row::with_children(vec![
|
2024-09-20 19:44:36 -06:00
|
|
|
widget::button::custom(icon_cache_get("edit-delete-symbolic", 16))
|
2024-02-09 15:45:46 -07:00
|
|
|
.on_press(Message::ProfileRemove(profile_id))
|
2024-10-22 12:31:02 -06:00
|
|
|
.class(style::Button::Icon)
|
2024-02-09 15:45:46 -07:00
|
|
|
.into(),
|
|
|
|
|
if expanded {
|
2024-09-20 19:44:36 -06:00
|
|
|
widget::button::custom(icon_cache_get("go-up-symbolic", 16))
|
2024-02-09 15:45:46 -07:00
|
|
|
.on_press(Message::ProfileCollapse(profile_id))
|
|
|
|
|
} else {
|
2024-09-20 19:44:36 -06:00
|
|
|
widget::button::custom(icon_cache_get("go-down-symbolic", 16))
|
2024-02-09 15:45:46 -07:00
|
|
|
.on_press(Message::ProfileExpand(profile_id))
|
|
|
|
|
}
|
2024-10-22 12:31:02 -06:00
|
|
|
.class(style::Button::Icon)
|
2024-02-09 15:45:46 -07:00
|
|
|
.into(),
|
|
|
|
|
])
|
2024-10-22 12:31:02 -06:00
|
|
|
.align_y(Alignment::Center)
|
2024-02-09 15:45:46 -07:00
|
|
|
.spacing(space_xxs),
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if expanded {
|
|
|
|
|
let dark_selected = self
|
2024-02-22 11:18:13 -07:00
|
|
|
.theme_names_dark
|
2024-02-09 15:45:46 -07:00
|
|
|
.iter()
|
|
|
|
|
.position(|theme_name| theme_name == &profile.syntax_theme_dark);
|
|
|
|
|
let light_selected = self
|
2024-02-22 11:18:13 -07:00
|
|
|
.theme_names_light
|
2024-02-09 15:45:46 -07:00
|
|
|
.iter()
|
|
|
|
|
.position(|theme_name| theme_name == &profile.syntax_theme_light);
|
|
|
|
|
|
2024-10-22 12:31:02 -06:00
|
|
|
let expanded_section = widget::settings::section()
|
2024-02-09 15:45:46 -07:00
|
|
|
.add(
|
|
|
|
|
widget::column::with_children(vec![
|
|
|
|
|
widget::column::with_children(vec![
|
|
|
|
|
widget::text(fl!("name")).into(),
|
|
|
|
|
widget::text_input("", &profile.name)
|
|
|
|
|
.on_input(move |text| {
|
|
|
|
|
Message::ProfileName(profile_id, text)
|
|
|
|
|
})
|
2025-08-12 12:54:26 -04:00
|
|
|
.on_paste(move |text| {
|
|
|
|
|
Message::ProfileName(profile_id, text)
|
|
|
|
|
})
|
2024-02-09 15:45:46 -07:00
|
|
|
.into(),
|
|
|
|
|
])
|
|
|
|
|
.spacing(space_xxxs)
|
|
|
|
|
.into(),
|
|
|
|
|
widget::column::with_children(vec![
|
|
|
|
|
widget::text(fl!("command-line")).into(),
|
|
|
|
|
widget::text_input("", &profile.command)
|
|
|
|
|
.on_input(move |text| {
|
|
|
|
|
Message::ProfileCommand(profile_id, text)
|
|
|
|
|
})
|
2025-08-12 12:54:26 -04:00
|
|
|
.on_paste(move |text| {
|
|
|
|
|
Message::ProfileCommand(profile_id, text)
|
|
|
|
|
})
|
2024-02-09 15:45:46 -07:00
|
|
|
.into(),
|
|
|
|
|
])
|
|
|
|
|
.spacing(space_xxxs)
|
|
|
|
|
.into(),
|
2024-04-14 21:17:32 +02:00
|
|
|
widget::column::with_children(vec![
|
|
|
|
|
widget::text(fl!("working-directory")).into(),
|
|
|
|
|
widget::text_input("", &profile.working_directory)
|
|
|
|
|
.on_input(move |text| {
|
|
|
|
|
Message::ProfileDirectory(profile_id, text)
|
|
|
|
|
})
|
2025-08-12 12:54:26 -04:00
|
|
|
.on_paste(move |text| {
|
|
|
|
|
Message::ProfileDirectory(profile_id, text)
|
|
|
|
|
})
|
2024-04-14 21:17:32 +02:00
|
|
|
.into(),
|
|
|
|
|
])
|
|
|
|
|
.spacing(space_xxxs)
|
|
|
|
|
.into(),
|
2024-02-09 15:45:46 -07:00
|
|
|
widget::column::with_children(vec![
|
|
|
|
|
widget::text(fl!("tab-title")).into(),
|
|
|
|
|
widget::text_input("", &profile.tab_title)
|
|
|
|
|
.on_input(move |text| {
|
|
|
|
|
Message::ProfileTabTitle(profile_id, text)
|
|
|
|
|
})
|
2025-08-12 12:54:26 -04:00
|
|
|
.on_paste(move |text| {
|
|
|
|
|
Message::ProfileTabTitle(profile_id, text)
|
|
|
|
|
})
|
2024-02-09 15:45:46 -07:00
|
|
|
.into(),
|
|
|
|
|
widget::text::caption(fl!("tab-title-description")).into(),
|
|
|
|
|
])
|
|
|
|
|
.spacing(space_xxxs)
|
|
|
|
|
.into(),
|
|
|
|
|
])
|
|
|
|
|
.padding([0, space_s])
|
|
|
|
|
.spacing(space_xs),
|
|
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
//TODO: rename to color-scheme-dark?
|
|
|
|
|
widget::settings::item::builder(fl!("syntax-dark")).control(
|
2025-03-19 12:27:00 -04:00
|
|
|
widget::dropdown::popup_dropdown(
|
2024-02-22 11:18:13 -07:00
|
|
|
&self.theme_names_dark,
|
2024-02-09 15:45:46 -07:00
|
|
|
dark_selected,
|
|
|
|
|
move |theme_i| {
|
2024-02-22 11:18:13 -07:00
|
|
|
Message::ProfileSyntaxTheme(
|
|
|
|
|
profile_id,
|
|
|
|
|
ColorSchemeKind::Dark,
|
|
|
|
|
theme_i,
|
|
|
|
|
)
|
2024-02-09 15:45:46 -07:00
|
|
|
},
|
2025-03-19 12:27:00 -04:00
|
|
|
self.core.main_window_id().unwrap_or(window::Id::RESERVED),
|
|
|
|
|
Message::Surface,
|
|
|
|
|
|a| a,
|
2024-02-09 15:45:46 -07:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
//TODO: rename to color-scheme-light?
|
|
|
|
|
widget::settings::item::builder(fl!("syntax-light")).control(
|
|
|
|
|
widget::dropdown(
|
2024-02-22 11:18:13 -07:00
|
|
|
&self.theme_names_light,
|
2024-02-09 15:45:46 -07:00
|
|
|
light_selected,
|
|
|
|
|
move |theme_i| {
|
2024-02-22 11:18:13 -07:00
|
|
|
Message::ProfileSyntaxTheme(
|
|
|
|
|
profile_id,
|
|
|
|
|
ColorSchemeKind::Light,
|
|
|
|
|
theme_i,
|
|
|
|
|
)
|
2024-02-09 15:45:46 -07:00
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
2024-02-26 04:47:52 +01:00
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("make-default")).control(
|
|
|
|
|
widget::toggler(
|
|
|
|
|
self.get_default_profile().is_some_and(|p| p == profile_id),
|
2024-10-22 12:31:02 -06:00
|
|
|
)
|
|
|
|
|
.on_toggle(move |t| Message::UpdateDefaultProfile((t, profile_id))),
|
2024-02-26 04:47:52 +01:00
|
|
|
),
|
2024-04-14 21:17:32 +02:00
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
widget::row::with_children(vec![
|
|
|
|
|
widget::column::with_children(vec![
|
|
|
|
|
widget::text(fl!("hold")).into(),
|
|
|
|
|
widget::text::caption(fl!("remain-open")).into(),
|
|
|
|
|
])
|
|
|
|
|
.spacing(space_xxxs)
|
|
|
|
|
.into(),
|
2026-03-18 12:21:57 -04:00
|
|
|
widget::space::horizontal().into(),
|
2025-11-06 12:33:54 +01:00
|
|
|
widget::toggler(profile.drain_on_exit)
|
2024-10-22 12:31:02 -06:00
|
|
|
.on_toggle(move |t| Message::ProfileHold(profile_id, t))
|
|
|
|
|
.into(),
|
2024-04-14 21:17:32 +02:00
|
|
|
])
|
2024-10-22 12:31:02 -06:00
|
|
|
.align_y(Alignment::Center)
|
2024-04-14 21:17:32 +02:00
|
|
|
.padding([0, space_s]),
|
2024-02-09 15:45:46 -07:00
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let padding = Padding {
|
|
|
|
|
top: 0.0,
|
|
|
|
|
bottom: 0.0,
|
2024-04-15 20:44:25 +01:00
|
|
|
left: space_s.into(),
|
|
|
|
|
right: space_s.into(),
|
2024-02-09 15:45:46 -07:00
|
|
|
};
|
|
|
|
|
profiles_section =
|
|
|
|
|
profiles_section.add(widget::container(expanded_section).padding(padding))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sections.push(profiles_section.into());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let add_profile = widget::row::with_children(vec![
|
2026-03-18 12:21:57 -04:00
|
|
|
widget::space::horizontal().into(),
|
2024-02-22 09:55:46 -07:00
|
|
|
widget::button::standard(fl!("add-profile"))
|
2024-02-09 15:45:46 -07:00
|
|
|
.on_press(Message::ProfileNew)
|
|
|
|
|
.into(),
|
|
|
|
|
]);
|
|
|
|
|
sections.push(add_profile.into());
|
|
|
|
|
|
|
|
|
|
widget::settings::view_column(sections).into()
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 14:33:46 +02:00
|
|
|
fn settings(&self) -> Element<'_, Message> {
|
2026-07-06 09:26:53 -04:00
|
|
|
let t = self.core().system_theme();
|
2023-12-22 14:31:01 -07:00
|
|
|
let app_theme_selected = match self.config.app_theme {
|
|
|
|
|
AppTheme::Dark => 1,
|
|
|
|
|
AppTheme::Light => 2,
|
|
|
|
|
AppTheme::System => 0,
|
|
|
|
|
};
|
|
|
|
|
let dark_selected = self
|
2024-02-22 11:18:13 -07:00
|
|
|
.theme_names_dark
|
2023-12-22 14:31:01 -07:00
|
|
|
.iter()
|
|
|
|
|
.position(|theme_name| theme_name == &self.config.syntax_theme_dark);
|
|
|
|
|
let light_selected = self
|
2024-02-22 11:18:13 -07:00
|
|
|
.theme_names_light
|
2023-12-22 14:31:01 -07:00
|
|
|
.iter()
|
|
|
|
|
.position(|theme_name| theme_name == &self.config.syntax_theme_light);
|
|
|
|
|
let font_selected = {
|
|
|
|
|
let mut font_system = font_system().write().unwrap();
|
|
|
|
|
let current_font_name = font_system.raw().db().family_name(&Family::Monospace);
|
|
|
|
|
self.font_names
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|font_name| font_name == current_font_name)
|
|
|
|
|
};
|
|
|
|
|
let font_size_selected = self
|
|
|
|
|
.font_sizes
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|font_size| font_size == &self.config.font_size);
|
2024-01-10 01:19:04 +03:00
|
|
|
let font_stretch_selected = self
|
|
|
|
|
.curr_font_stretches
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|font_stretch| font_stretch == &self.config.typed_font_stretch());
|
|
|
|
|
let font_weight_selected = self
|
|
|
|
|
.curr_font_weights
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|font_weight| font_weight == &self.config.font_weight);
|
2024-01-12 01:58:57 +03:00
|
|
|
let dim_font_weight_selected = self
|
|
|
|
|
.curr_font_weights
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|font_weight| font_weight == &self.config.dim_font_weight);
|
2024-01-10 01:19:04 +03:00
|
|
|
let bold_font_weight_selected = self
|
|
|
|
|
.curr_font_weights
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|font_weight| font_weight == &self.config.bold_font_weight);
|
2024-01-01 12:27:51 +03:00
|
|
|
let zoom_step_selected = self
|
|
|
|
|
.zoom_steps
|
|
|
|
|
.iter()
|
|
|
|
|
.position(|zoom_step| zoom_step == &self.config.font_size_zoom_step_mul_100);
|
2024-01-10 01:38:13 +03:00
|
|
|
|
2024-10-22 12:31:02 -06:00
|
|
|
let appearance_section = widget::settings::section()
|
|
|
|
|
.title(fl!("appearance"))
|
2024-01-31 10:29:35 -07:00
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("theme")).control(widget::dropdown(
|
|
|
|
|
&self.app_themes,
|
|
|
|
|
Some(app_theme_selected),
|
|
|
|
|
move |index| {
|
|
|
|
|
Message::AppTheme(match index {
|
|
|
|
|
1 => AppTheme::Dark,
|
|
|
|
|
2 => AppTheme::Light,
|
|
|
|
|
_ => AppTheme::System,
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
)),
|
|
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
//TODO: rename to color-scheme-dark?
|
|
|
|
|
widget::settings::item::builder(fl!("syntax-dark")).control(widget::dropdown(
|
2024-02-22 11:18:13 -07:00
|
|
|
&self.theme_names_dark,
|
2024-01-31 10:29:35 -07:00
|
|
|
dark_selected,
|
2024-02-22 11:18:13 -07:00
|
|
|
move |index| Message::SyntaxTheme(ColorSchemeKind::Dark, index),
|
2024-01-31 10:29:35 -07:00
|
|
|
)),
|
|
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
//TODO: rename to color-scheme-light?
|
|
|
|
|
widget::settings::item::builder(fl!("syntax-light")).control(widget::dropdown(
|
2024-02-22 11:18:13 -07:00
|
|
|
&self.theme_names_light,
|
2024-01-31 10:29:35 -07:00
|
|
|
light_selected,
|
2024-02-22 11:18:13 -07:00
|
|
|
move |index| Message::SyntaxTheme(ColorSchemeKind::Light, index),
|
2024-01-31 10:29:35 -07:00
|
|
|
)),
|
|
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("default-zoom-step")).control(
|
|
|
|
|
widget::dropdown(&self.zoom_step_names, zoom_step_selected, |index| {
|
|
|
|
|
Message::DefaultZoomStep(index)
|
|
|
|
|
}),
|
|
|
|
|
),
|
2024-02-12 12:36:30 -07:00
|
|
|
)
|
2026-07-06 09:26:53 -04:00
|
|
|
.add_maybe((!t.transparent).then(|| {
|
2024-02-12 12:36:30 -07:00
|
|
|
widget::settings::item::builder(fl!("opacity"))
|
|
|
|
|
.description(format!("{}%", self.config.opacity))
|
|
|
|
|
.control(widget::slider(0..=100, self.config.opacity, |opacity| {
|
|
|
|
|
Message::Opacity(opacity)
|
2026-07-06 09:26:53 -04:00
|
|
|
}))
|
|
|
|
|
}));
|
2024-01-31 10:29:35 -07:00
|
|
|
|
2024-10-22 12:31:02 -06:00
|
|
|
let mut font_section = widget::settings::section()
|
|
|
|
|
.title(fl!("font"))
|
2024-01-31 10:29:35 -07:00
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("default-font")).control(widget::dropdown(
|
|
|
|
|
&self.font_names,
|
|
|
|
|
font_selected,
|
2024-02-05 15:59:07 +01:00
|
|
|
Message::DefaultFont,
|
2024-01-31 10:29:35 -07:00
|
|
|
)),
|
|
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("default-font-size")).control(
|
|
|
|
|
widget::dropdown(&self.font_size_names, font_size_selected, |index| {
|
|
|
|
|
Message::DefaultFontSize(index)
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.add(
|
2024-02-22 11:31:10 -07:00
|
|
|
widget::settings::item::builder(fl!("advanced-font-settings")).control(
|
|
|
|
|
if self.show_advanced_font_settings {
|
2024-09-20 19:44:36 -06:00
|
|
|
widget::button::custom(icon_cache_get("go-up-symbolic", 16))
|
2024-02-22 11:31:10 -07:00
|
|
|
.on_press(Message::ShowAdvancedFontSettings(false))
|
|
|
|
|
} else {
|
2024-09-20 19:44:36 -06:00
|
|
|
widget::button::custom(icon_cache_get("go-down-symbolic", 16))
|
2024-02-22 11:31:10 -07:00
|
|
|
.on_press(Message::ShowAdvancedFontSettings(true))
|
2024-02-22 11:32:39 -07:00
|
|
|
}
|
2024-10-22 12:31:02 -06:00
|
|
|
.class(style::Button::Icon),
|
2024-01-31 10:29:35 -07:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
|
2024-01-10 01:38:13 +03:00
|
|
|
let advanced_font_settings = || {
|
2024-10-22 12:31:02 -06:00
|
|
|
let section = widget::settings::section()
|
2024-01-10 01:38:13 +03:00
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("default-font-stretch")).control(
|
2024-01-11 11:33:39 -07:00
|
|
|
widget::dropdown(
|
|
|
|
|
&self.curr_font_stretch_names,
|
|
|
|
|
font_stretch_selected,
|
2024-02-05 15:59:07 +01:00
|
|
|
Message::DefaultFontStretch,
|
2024-01-11 11:33:39 -07:00
|
|
|
),
|
2024-01-10 01:38:13 +03:00
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("default-font-weight")).control(
|
2024-01-11 11:33:39 -07:00
|
|
|
widget::dropdown(
|
|
|
|
|
&self.curr_font_weight_names,
|
|
|
|
|
font_weight_selected,
|
2024-02-05 15:59:07 +01:00
|
|
|
Message::DefaultFontWeight,
|
2024-01-11 11:33:39 -07:00
|
|
|
),
|
2024-01-10 01:38:13 +03:00
|
|
|
),
|
|
|
|
|
)
|
2024-01-12 01:58:57 +03:00
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("default-dim-font-weight")).control(
|
|
|
|
|
widget::dropdown(
|
|
|
|
|
&self.curr_font_weight_names,
|
|
|
|
|
dim_font_weight_selected,
|
2024-02-05 15:59:07 +01:00
|
|
|
Message::DefaultDimFontWeight,
|
2024-01-12 01:58:57 +03:00
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
)
|
2024-01-10 01:38:13 +03:00
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("default-bold-font-weight")).control(
|
2024-01-11 11:33:39 -07:00
|
|
|
widget::dropdown(
|
|
|
|
|
&self.curr_font_weight_names,
|
|
|
|
|
bold_font_weight_selected,
|
2024-02-05 15:59:07 +01:00
|
|
|
Message::DefaultBoldFontWeight,
|
2024-01-11 11:33:39 -07:00
|
|
|
),
|
2024-01-10 01:38:13 +03:00
|
|
|
),
|
2024-01-31 10:29:35 -07:00
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("use-bright-bold"))
|
|
|
|
|
.toggler(self.config.use_bright_bold, Message::UseBrightBold),
|
2024-01-10 01:38:13 +03:00
|
|
|
);
|
|
|
|
|
let padding = Padding {
|
|
|
|
|
top: 0.0,
|
|
|
|
|
bottom: 0.0,
|
|
|
|
|
left: 12.0,
|
|
|
|
|
right: 12.0,
|
|
|
|
|
};
|
|
|
|
|
widget::container(section).padding(padding)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if self.show_advanced_font_settings {
|
2024-01-31 10:29:35 -07:00
|
|
|
font_section = font_section.add(advanced_font_settings());
|
2024-01-10 01:38:13 +03:00
|
|
|
}
|
|
|
|
|
|
2026-04-06 18:38:10 -06:00
|
|
|
let splits_section = widget::settings::section()
|
|
|
|
|
.title(fl!("splits"))
|
|
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("focus-follow-mouse"))
|
|
|
|
|
.toggler(self.config.focus_follow_mouse, Message::FocusFollowMouse),
|
|
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("show-pane-borders"))
|
|
|
|
|
.toggler(self.config.show_pane_borders, Message::ShowPaneBorders),
|
yoda: attenuation des panneaux non focalises, a la Ghostty
Avec Claude Code et Modjo cote a cote, rien ne distinguait le panneau ou
l'on tape, hors le curseur creux. L'amont assombrissait le fond de 8 %
(aussi quand la fenetre perdait le focus) et l'a retire en juillet
(a43b5ae).
Reprise de unfocused-split-opacity de Ghostty : un voile de la couleur du
fond, texte compris, sur chaque panneau non focalise, seulement quand il y
en a plusieurs ; la perte de focus de la fenetre ne change rien.
- Config unfocused_pane_dim (pourcentage, defaut 30 = opacite 0,7 de
Ghostty, 0 desactive, plafond 85 = opacite minimale 0,15 de Ghostty).
- Reglages > Panneaux : curseur « Attenuation des panneaux inactifs ».
- terminal_box : voile trace dans un calque a part en fin de rendu (dans
le meme calque, la primitive de texte passerait par-dessus), aux coins
du fond du panneau.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:40:47 +02:00
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("unfocused-pane-dim"))
|
|
|
|
|
.description(format!("{}%", self.config.unfocused_pane_dim))
|
|
|
|
|
.control(widget::slider(
|
|
|
|
|
0..=config::UNFOCUSED_PANE_DIM_MAX,
|
|
|
|
|
self.config.unfocused_pane_dim,
|
|
|
|
|
Message::UnfocusedPaneDim,
|
|
|
|
|
)),
|
2026-04-06 18:38:10 -06:00
|
|
|
);
|
2024-01-31 10:29:35 -07:00
|
|
|
|
2026-03-06 13:53:46 +01:00
|
|
|
let advanced_section = widget::settings::section()
|
|
|
|
|
.title(fl!("advanced"))
|
|
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("show-headerbar"))
|
|
|
|
|
.description(fl!("show-header-description"))
|
|
|
|
|
.toggler(self.config.show_headerbar, Message::ShowHeaderBar),
|
|
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("tab-new-inherit-working-directory"))
|
|
|
|
|
.description(fl!("tab-new-inherit-working-directory-description"))
|
|
|
|
|
.toggler(
|
|
|
|
|
self.config.tab_new_inherit_working_directory,
|
|
|
|
|
Message::TabNewInheritWorkingDirectory,
|
|
|
|
|
),
|
2026-10-01 14:38:17 +02:00
|
|
|
)
|
|
|
|
|
.add(
|
|
|
|
|
widget::settings::item::builder(fl!("copy-on-select-clipboard"))
|
|
|
|
|
.description(fl!("copy-on-select-clipboard-description"))
|
|
|
|
|
.toggler(
|
|
|
|
|
self.config.copy_on_select_clipboard,
|
|
|
|
|
Message::CopyOnSelectClipboard,
|
|
|
|
|
),
|
2026-03-06 13:53:46 +01:00
|
|
|
);
|
2024-01-31 10:29:35 -07:00
|
|
|
|
|
|
|
|
widget::settings::view_column(vec![
|
|
|
|
|
appearance_section.into(),
|
|
|
|
|
font_section.into(),
|
|
|
|
|
splits_section.into(),
|
|
|
|
|
advanced_section.into(),
|
|
|
|
|
])
|
|
|
|
|
.into()
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
2024-02-26 04:47:52 +01:00
|
|
|
fn get_default_profile(&self) -> Option<ProfileId> {
|
|
|
|
|
self.config.default_profile
|
|
|
|
|
}
|
2024-01-18 10:14:32 +01:00
|
|
|
|
2026-03-06 13:53:46 +01:00
|
|
|
fn active_terminal_working_directory(&self) -> Option<PathBuf> {
|
|
|
|
|
let tab_model = self.pane_model.active()?;
|
|
|
|
|
let entity = tab_model.active();
|
|
|
|
|
let terminal = tab_model.data::<Mutex<Terminal>>(entity)?;
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.working_directory()
|
|
|
|
|
}
|
|
|
|
|
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
/// Build the regex pattern for the find bar based on the current options.
|
|
|
|
|
fn find_pattern(&self) -> String {
|
|
|
|
|
let mut pattern = if self.find_regex {
|
|
|
|
|
self.find_search_value.clone()
|
|
|
|
|
} else {
|
|
|
|
|
regex::escape(&self.find_search_value)
|
|
|
|
|
};
|
|
|
|
|
if !self.find_case_sensitive {
|
|
|
|
|
pattern = format!("(?i){pattern}");
|
|
|
|
|
}
|
|
|
|
|
pattern
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn terminal_monitors(
|
|
|
|
|
&self,
|
|
|
|
|
pane: pane_grid::Pane,
|
|
|
|
|
entity: segmented_button::Entity,
|
|
|
|
|
) -> (bool, bool, bool) {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.panes.get(pane)
|
|
|
|
|
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
{
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
(
|
|
|
|
|
terminal.monitor_activity,
|
|
|
|
|
terminal.monitor_silence,
|
|
|
|
|
terminal.monitor_process_finished,
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
(false, false, false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)
Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.
- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
plus la vue en bas. Couvre touches legacy et kitty, collage, clic
milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
application attendrait une reponse qui ne vient pas (CSI ? u de Claude
Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
(selection, copie et defilement locaux, meme dans une application qui
la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
action ToggleReadOnly dans les raccourcis (groupe Autre, sans
raccourci par defaut, comme Ghostty et Konsole) ; cadenas
changes-prevent-symbolic sur l'onglet, apres les alertes.
Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:19:02 +02:00
|
|
|
fn terminal_read_only(&self, pane: pane_grid::Pane, entity: segmented_button::Entity) -> bool {
|
|
|
|
|
self.pane_model
|
|
|
|
|
.panes
|
|
|
|
|
.get(pane)
|
|
|
|
|
.and_then(|tab_model| tab_model.data::<Mutex<Terminal>>(entity))
|
|
|
|
|
.is_some_and(|terminal| terminal.lock().unwrap().read_only)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn active_terminal_read_only(&self) -> bool {
|
|
|
|
|
let pane = self.pane_model.focused();
|
|
|
|
|
self.pane_model
|
|
|
|
|
.panes
|
|
|
|
|
.get(pane)
|
|
|
|
|
.is_some_and(|tab_model| self.terminal_read_only(pane, tab_model.active()))
|
|
|
|
|
}
|
|
|
|
|
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
fn active_terminal_monitors(&self) -> (bool, bool, bool) {
|
|
|
|
|
let pane = self.pane_model.focused();
|
|
|
|
|
if let Some(tab_model) = self.pane_model.panes.get(pane) {
|
|
|
|
|
self.terminal_monitors(pane, tab_model.active())
|
|
|
|
|
} else {
|
|
|
|
|
(false, false, false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn any_monitor_needs_tick(&self) -> bool {
|
|
|
|
|
self.pane_model.panes.iter().any(|(_pane, tab_model)| {
|
|
|
|
|
tab_model.iter().any(|entity| {
|
|
|
|
|
tab_model
|
|
|
|
|
.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
.is_some_and(|terminal| {
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.monitor_silence || terminal.monitor_process_finished
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-09 14:18:02 +02:00
|
|
|
/// 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)
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-09 15:45:46 -07:00
|
|
|
fn create_and_focus_new_terminal(
|
|
|
|
|
&mut self,
|
|
|
|
|
pane: pane_grid::Pane,
|
|
|
|
|
profile_id_opt: Option<ProfileId>,
|
2026-03-06 13:53:46 +01:00
|
|
|
inherit_working_directory: bool,
|
2024-10-22 12:31:02 -06:00
|
|
|
) -> Task<Message> {
|
2026-03-06 13:53:46 +01:00
|
|
|
let inherited_working_directory = inherit_working_directory
|
|
|
|
|
.then(|| self.active_terminal_working_directory())
|
|
|
|
|
.flatten();
|
2025-01-21 17:07:45 +01:00
|
|
|
self.pane_model.set_focus(pane);
|
2024-01-18 10:14:32 +01:00
|
|
|
match &self.term_event_tx_opt {
|
2024-02-09 15:45:46 -07:00
|
|
|
Some(term_event_tx) => {
|
2026-06-12 13:47:00 -07:00
|
|
|
let color_scheme_kind = self.config.color_scheme_kind(self.core.system_theme());
|
2024-04-12 13:26:08 +02:00
|
|
|
let colors = self
|
|
|
|
|
.themes
|
2026-06-12 13:47:00 -07:00
|
|
|
.get(&self.config.syntax_theme(color_scheme_kind, profile_id_opt))
|
|
|
|
|
.or_else(|| match color_scheme_kind {
|
2024-04-14 16:48:13 +02:00
|
|
|
ColorSchemeKind::Dark => self
|
|
|
|
|
.themes
|
|
|
|
|
.get(&(config::COSMIC_THEME_DARK.to_string(), ColorSchemeKind::Dark)),
|
|
|
|
|
ColorSchemeKind::Light => self.themes.get(&(
|
|
|
|
|
config::COSMIC_THEME_LIGHT.to_string(),
|
|
|
|
|
ColorSchemeKind::Light,
|
|
|
|
|
)),
|
2024-04-12 13:26:08 +02:00
|
|
|
});
|
|
|
|
|
match colors {
|
2024-02-09 15:45:46 -07:00
|
|
|
Some(colors) => {
|
2025-01-21 17:07:45 +01:00
|
|
|
let current_pane = self.pane_model.focused();
|
2024-02-09 15:45:46 -07:00
|
|
|
if let Some(tab_model) = self.pane_model.active_mut() {
|
2026-02-28 22:04:01 +01:00
|
|
|
let (options, tab_title_override) = if let Some(profile) =
|
|
|
|
|
profile_id_opt
|
|
|
|
|
.and_then(|profile_id| self.config.profiles.get(&profile_id))
|
2026-02-16 23:32:19 +00:00
|
|
|
{
|
|
|
|
|
// Merge profile and startup options, preferring startup options
|
2026-02-28 22:04:01 +01:00
|
|
|
let startup_options =
|
|
|
|
|
self.startup_options.take().unwrap_or_default();
|
2026-02-16 23:47:47 +00:00
|
|
|
let options = tty::Options {
|
|
|
|
|
shell: startup_options.shell.or_else(|| {
|
2026-04-10 18:09:25 +02:00
|
|
|
if let Some(mut args) = shlex::split(&profile.command)
|
2026-04-10 18:32:09 +02:00
|
|
|
&& !args.is_empty()
|
|
|
|
|
{
|
|
|
|
|
let command = args.remove(0);
|
|
|
|
|
return Some(tty::Shell::new(command, args));
|
2024-02-09 15:45:46 -07:00
|
|
|
}
|
2026-04-10 18:09:25 +02:00
|
|
|
None
|
2026-02-16 23:32:19 +00:00
|
|
|
}),
|
2026-03-06 13:53:46 +01:00
|
|
|
working_directory: startup_options
|
|
|
|
|
.working_directory
|
|
|
|
|
.or_else(|| inherited_working_directory.clone())
|
|
|
|
|
.or_else(|| {
|
2026-02-28 22:04:01 +01:00
|
|
|
(!profile.working_directory.is_empty())
|
|
|
|
|
.then(|| profile.working_directory.clone().into())
|
2026-03-06 13:53:46 +01:00
|
|
|
}),
|
2026-02-28 22:04:01 +01:00
|
|
|
drain_on_exit: startup_options.drain_on_exit
|
|
|
|
|
|| profile.drain_on_exit,
|
2026-02-16 23:47:47 +00:00
|
|
|
..startup_options
|
2026-02-16 23:32:19 +00:00
|
|
|
};
|
2026-02-16 23:47:47 +00:00
|
|
|
let tab_title_override = if profile.tab_title.is_empty() {
|
2026-02-16 23:32:19 +00:00
|
|
|
None
|
|
|
|
|
} else {
|
|
|
|
|
Some(profile.tab_title.clone())
|
|
|
|
|
};
|
2026-02-16 23:47:47 +00:00
|
|
|
(options, tab_title_override)
|
|
|
|
|
} else {
|
2026-03-06 13:53:46 +01:00
|
|
|
let mut options = self.startup_options.take().unwrap_or_default();
|
|
|
|
|
if options.working_directory.is_none() {
|
|
|
|
|
options.working_directory = inherited_working_directory.clone();
|
|
|
|
|
}
|
|
|
|
|
(options, None)
|
2024-02-09 15:45:46 -07:00
|
|
|
};
|
2026-02-16 23:32:19 +00:00
|
|
|
|
2024-02-20 10:21:30 -07:00
|
|
|
let entity = tab_model
|
|
|
|
|
.insert()
|
|
|
|
|
.text(
|
|
|
|
|
tab_title_override
|
|
|
|
|
.clone()
|
|
|
|
|
.unwrap_or_else(|| fl!("new-terminal")),
|
|
|
|
|
)
|
|
|
|
|
.closable()
|
|
|
|
|
.activate()
|
|
|
|
|
.id();
|
2024-02-09 15:45:46 -07:00
|
|
|
match Terminal::new(
|
|
|
|
|
current_pane,
|
|
|
|
|
entity,
|
|
|
|
|
term_event_tx.clone(),
|
|
|
|
|
self.term_config.clone(),
|
|
|
|
|
options,
|
|
|
|
|
&self.config,
|
|
|
|
|
*colors,
|
|
|
|
|
profile_id_opt,
|
2024-02-20 10:21:30 -07:00
|
|
|
tab_title_override,
|
2024-02-09 15:45:46 -07:00
|
|
|
) {
|
|
|
|
|
Ok(mut terminal) => {
|
2026-06-12 13:47:00 -07:00
|
|
|
terminal.set_config(
|
|
|
|
|
&self.config,
|
|
|
|
|
color_scheme_kind,
|
|
|
|
|
&self.themes,
|
|
|
|
|
);
|
2024-02-09 15:45:46 -07:00
|
|
|
tab_model
|
|
|
|
|
.data_set::<Mutex<Terminal>>(entity, Mutex::new(terminal));
|
|
|
|
|
}
|
2024-08-13 02:20:52 -04:00
|
|
|
Err(err) if profile_id_opt.is_some() => {
|
|
|
|
|
// Create a tab without a profile if the selected
|
|
|
|
|
// profile doesn't work
|
|
|
|
|
let name = profile_id_opt
|
|
|
|
|
.and_then(|id| self.config.profiles.get(&id))
|
|
|
|
|
.map(|profile| profile.name.as_str())
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
log::error!(
|
|
|
|
|
"failed to open terminal with profile `{}`: {}",
|
|
|
|
|
name,
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// TabClose focuses the nearest tab which would be incorrect
|
|
|
|
|
// in this specific case as it would unfocus the new tab
|
|
|
|
|
// created by TabNewNoProfile
|
|
|
|
|
// TabClose can also cause the terminal app to close if it
|
|
|
|
|
// closes the only open tab. This would close cosmic term
|
|
|
|
|
// if launched with an invalid profile (issue #274)
|
|
|
|
|
tab_model.remove(entity);
|
|
|
|
|
return self.update(Message::TabNewNoProfile);
|
|
|
|
|
}
|
2024-02-09 15:45:46 -07:00
|
|
|
Err(err) => {
|
|
|
|
|
log::error!("failed to open terminal: {}", err);
|
|
|
|
|
// Clean up partially created tab
|
|
|
|
|
return self.update(Message::TabClose(Some(entity)));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
log::error!("Found no active pane");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
log::error!(
|
|
|
|
|
"failed to find terminal theme {:?}",
|
2026-06-12 13:47:00 -07:00
|
|
|
self.config.syntax_theme(color_scheme_kind, profile_id_opt)
|
2024-01-18 10:14:32 +01:00
|
|
|
);
|
2024-02-09 15:45:46 -07:00
|
|
|
//TODO: fall back to known good theme
|
2024-01-18 10:14:32 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-09 15:45:46 -07:00
|
|
|
}
|
2024-01-18 10:14:32 +01:00
|
|
|
None => {
|
|
|
|
|
log::warn!("tried to create new tab before having event channel");
|
|
|
|
|
}
|
|
|
|
|
}
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
self.sync_terminal_focus();
|
2024-04-14 21:02:01 +01:00
|
|
|
self.update_title(Some(pane))
|
2024-01-18 10:14:32 +01:00
|
|
|
}
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Implement [`Application`] to integrate with COSMIC.
|
|
|
|
|
impl Application for App {
|
2023-12-20 13:31:10 -07:00
|
|
|
/// Default async executor to use with the app.
|
|
|
|
|
type Executor = executor::Default;
|
|
|
|
|
|
2023-12-21 10:19:16 -07:00
|
|
|
/// Argument received
|
2023-12-22 14:31:01 -07:00
|
|
|
type Flags = Flags;
|
2023-12-20 13:31:10 -07:00
|
|
|
|
|
|
|
|
/// Message type specific to our [`App`].
|
|
|
|
|
type Message = Message;
|
2023-12-17 11:53:26 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
/// The unique application ID to supply to the window manager.
|
2023-12-22 14:31:01 -07:00
|
|
|
const APP_ID: &'static str = "com.system76.CosmicTerm";
|
2023-12-20 13:31:10 -07:00
|
|
|
|
|
|
|
|
fn core(&self) -> &Core {
|
|
|
|
|
&self.core
|
2023-12-17 11:53:26 -07:00
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
fn core_mut(&mut self) -> &mut Core {
|
|
|
|
|
&mut self.core
|
|
|
|
|
}
|
2023-12-17 22:51:50 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
/// Creates the application, and optionally emits command on initialize.
|
2024-10-22 12:31:02 -06:00
|
|
|
fn init(mut core: Core, flags: Self::Flags) -> (Self, Task<Self::Message>) {
|
2024-02-12 12:36:30 -07:00
|
|
|
core.window.content_container = false;
|
2023-12-22 15:40:10 -07:00
|
|
|
core.window.show_headerbar = flags.config.show_headerbar;
|
2023-12-21 11:57:52 -07:00
|
|
|
|
2023-12-22 14:31:01 -07:00
|
|
|
// Update font name from config
|
|
|
|
|
{
|
|
|
|
|
let mut font_system = font_system().write().unwrap();
|
|
|
|
|
font_system
|
|
|
|
|
.raw()
|
|
|
|
|
.db_mut()
|
|
|
|
|
.set_monospace_family(&flags.config.font_name);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let app_themes = vec![fl!("match-desktop"), fl!("dark"), fl!("light")];
|
|
|
|
|
|
2024-01-10 01:19:04 +03:00
|
|
|
let font_name_faces_map = {
|
|
|
|
|
let mut font_name_faces_map = BTreeMap::<_, Vec<_>>::new();
|
2023-12-22 14:31:01 -07:00
|
|
|
let mut font_system = font_system().write().unwrap();
|
|
|
|
|
//TODO: do not repeat, used in Tab::new
|
|
|
|
|
for face in font_system.raw().db().faces() {
|
2024-01-10 01:19:04 +03:00
|
|
|
// only monospace fonts and weights that match named constants.
|
|
|
|
|
let weight = face.weight.0;
|
2024-01-11 11:33:39 -07:00
|
|
|
if face.monospaced && { 1..9 }.contains(&{ weight / 100 }) && weight % 100 == 0 {
|
2023-12-22 14:31:01 -07:00
|
|
|
//TODO: get localized name if possible
|
|
|
|
|
let font_name = face
|
|
|
|
|
.families
|
2024-02-05 15:59:07 +01:00
|
|
|
.first()
|
2023-12-22 14:31:01 -07:00
|
|
|
.map_or_else(|| face.post_script_name.to_string(), |x| x.0.to_string());
|
2024-01-11 11:33:39 -07:00
|
|
|
font_name_faces_map
|
|
|
|
|
.entry(font_name)
|
|
|
|
|
.or_default()
|
|
|
|
|
.push(face.clone());
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-10 01:19:04 +03:00
|
|
|
|
|
|
|
|
// only keep fonts that have both NORMAL and BOLD weights with both having
|
|
|
|
|
// a `Stretch::Normal` face.
|
|
|
|
|
// This is important for fallbacks.
|
|
|
|
|
font_name_faces_map.retain(|_, v| {
|
2024-01-11 11:33:39 -07:00
|
|
|
let has_normal = v
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|face| face.weight == Weight::NORMAL && face.stretch == Stretch::Normal);
|
|
|
|
|
let has_bold = v
|
|
|
|
|
.iter()
|
|
|
|
|
.any(|face| face.weight == Weight::BOLD && face.stretch == Stretch::Normal);
|
2024-01-10 01:19:04 +03:00
|
|
|
has_normal && has_bold
|
|
|
|
|
});
|
|
|
|
|
font_name_faces_map
|
2023-12-22 14:31:01 -07:00
|
|
|
};
|
2024-09-03 01:11:17 +03:00
|
|
|
|
|
|
|
|
if font_name_faces_map.is_empty() {
|
2025-09-10 14:33:46 +02:00
|
|
|
log::error!(
|
|
|
|
|
"at least one monospace font with normal/bold weights and default stretch is required"
|
|
|
|
|
);
|
2024-09-03 01:11:17 +03:00
|
|
|
log::error!("no monospace fonts to select from, exiting");
|
|
|
|
|
process::exit(1);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-10 01:19:04 +03:00
|
|
|
let font_names = font_name_faces_map.keys().cloned().collect();
|
2023-12-22 14:31:01 -07:00
|
|
|
|
|
|
|
|
let mut font_size_names = Vec::new();
|
|
|
|
|
let mut font_sizes = Vec::new();
|
|
|
|
|
for font_size in 4..=32 {
|
2024-04-14 20:53:23 +01:00
|
|
|
font_size_names.push(format!("{font_size}px"));
|
2023-12-22 14:31:01 -07:00
|
|
|
font_sizes.push(font_size);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-10 01:19:04 +03:00
|
|
|
let mut all_font_weights_vals_names_map = BTreeMap::new();
|
|
|
|
|
|
|
|
|
|
macro_rules! populate_font_weights {
|
|
|
|
|
($($weight:ident,)+) => {
|
|
|
|
|
// all weights
|
|
|
|
|
paste::paste!{
|
|
|
|
|
$(
|
|
|
|
|
all_font_weights_vals_names_map
|
|
|
|
|
.insert(Weight::$weight.0, stringify!([<$weight:camel>]).into());
|
|
|
|
|
)+
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-11 11:33:39 -07:00
|
|
|
populate_font_weights! {
|
2024-01-10 01:19:04 +03:00
|
|
|
THIN, EXTRA_LIGHT, LIGHT, NORMAL, MEDIUM,
|
|
|
|
|
SEMIBOLD, BOLD, EXTRA_BOLD, BLACK,
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-11 11:33:39 -07:00
|
|
|
let mut all_font_stretches_vals_names_map = BTreeMap::new();
|
2024-01-10 01:19:04 +03:00
|
|
|
|
|
|
|
|
macro_rules! populate_font_stretches {
|
|
|
|
|
($($stretch:ident,)+) => {
|
|
|
|
|
// all stretches
|
|
|
|
|
$(
|
|
|
|
|
all_font_stretches_vals_names_map
|
|
|
|
|
.insert(Stretch::$stretch, stringify!($stretch).into());
|
|
|
|
|
)+
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-11 11:33:39 -07:00
|
|
|
populate_font_stretches! {
|
2024-01-10 01:19:04 +03:00
|
|
|
UltraCondensed, ExtraCondensed, Condensed, SemiCondensed,
|
|
|
|
|
Normal, SemiExpanded, Expanded, ExtraExpanded, UltraExpanded,
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-01 12:27:51 +03:00
|
|
|
let mut zoom_step_names = Vec::new();
|
|
|
|
|
let mut zoom_steps = Vec::new();
|
|
|
|
|
for zoom_step in [25, 50, 75, 100, 150, 200] {
|
|
|
|
|
zoom_step_names.push(format!("{}px", f32::from(zoom_step) / 100.0));
|
|
|
|
|
zoom_steps.push(zoom_step);
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-17 16:09:56 +01:00
|
|
|
let pane_model = TerminalPaneGrid::new(segmented_button::ModelBuilder::default().build());
|
|
|
|
|
let mut terminal_ids = HashMap::new();
|
2025-01-21 17:07:45 +01:00
|
|
|
terminal_ids.insert(pane_model.focused(), widget::Id::unique());
|
2023-12-22 14:31:01 -07:00
|
|
|
|
2025-09-11 16:25:29 +02:00
|
|
|
let about = About::default()
|
|
|
|
|
.name(fl!("cosmic-terminal"))
|
|
|
|
|
.icon(widget::icon::from_name(Self::APP_ID))
|
|
|
|
|
.version(env!("CARGO_PKG_VERSION"))
|
|
|
|
|
.author("System76")
|
2026-02-12 15:23:04 -07:00
|
|
|
.comments(fl!("comment"))
|
2025-09-11 16:25:29 +02:00
|
|
|
.license("GPL-3.0-only")
|
2025-10-03 15:32:16 -04:00
|
|
|
.license_url("https://spdx.org/licenses/GPL-3.0-only")
|
2025-09-11 16:25:29 +02:00
|
|
|
.developers([("Jeremy Soller", "jeremy@system76.com")])
|
|
|
|
|
.links([
|
|
|
|
|
(fl!("repository"), "https://github.com/pop-os/cosmic-term"),
|
|
|
|
|
(
|
|
|
|
|
fl!("support"),
|
|
|
|
|
"https://github.com/pop-os/cosmic-term/issues",
|
|
|
|
|
),
|
|
|
|
|
]);
|
|
|
|
|
|
2025-12-29 20:42:52 -08:00
|
|
|
let key_binds = key_binds(&flags.shortcuts_config);
|
2024-04-15 21:05:30 +01:00
|
|
|
let mut app = Self {
|
2023-12-20 13:31:10 -07:00
|
|
|
core,
|
2025-09-11 16:25:29 +02:00
|
|
|
about,
|
2024-01-17 16:09:56 +01:00
|
|
|
pane_model,
|
2023-12-22 14:31:01 -07:00
|
|
|
config_handler: flags.config_handler,
|
|
|
|
|
config: flags.config,
|
2025-12-29 20:42:52 -08:00
|
|
|
shortcuts_config: flags.shortcuts_config,
|
|
|
|
|
key_binds,
|
2023-12-22 14:31:01 -07:00
|
|
|
app_themes,
|
|
|
|
|
font_names,
|
|
|
|
|
font_size_names,
|
|
|
|
|
font_sizes,
|
2024-01-10 01:19:04 +03:00
|
|
|
font_name_faces_map,
|
|
|
|
|
all_font_weights_vals_names_map,
|
|
|
|
|
all_font_stretches_vals_names_map,
|
|
|
|
|
curr_font_weight_names: Vec::new(),
|
|
|
|
|
curr_font_weights: Vec::new(),
|
|
|
|
|
curr_font_stretch_names: Vec::new(),
|
|
|
|
|
curr_font_stretches: Vec::new(),
|
2024-01-01 12:27:51 +03:00
|
|
|
zoom_step_names,
|
|
|
|
|
zoom_steps,
|
2024-02-22 11:18:13 -07:00
|
|
|
theme_names_dark: Vec::new(),
|
|
|
|
|
theme_names_light: Vec::new(),
|
2024-02-22 09:55:46 -07:00
|
|
|
themes: HashMap::new(),
|
2023-12-22 14:31:01 -07:00
|
|
|
context_page: ContextPage::Settings,
|
2024-02-22 09:55:46 -07:00
|
|
|
dialog_opt: None,
|
2024-01-17 16:09:56 +01:00
|
|
|
terminal_ids,
|
2024-01-11 13:20:16 -07:00
|
|
|
find: false,
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
find_case_sensitive: false,
|
|
|
|
|
find_regex: false,
|
2024-01-11 13:20:16 -07:00
|
|
|
find_search_id: widget::Id::unique(),
|
|
|
|
|
find_search_value: String::new(),
|
2024-01-14 12:33:28 -07:00
|
|
|
startup_options: flags.startup_options,
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
startup_profile_id: flags.startup_profile_id,
|
2023-12-22 14:31:01 -07:00
|
|
|
term_config: flags.term_config,
|
2023-12-20 13:31:10 -07:00
|
|
|
term_event_tx_opt: None,
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
notify_tx_opt: None,
|
|
|
|
|
window_focused: true,
|
2024-02-22 09:55:46 -07:00
|
|
|
color_scheme_errors: Vec::new(),
|
|
|
|
|
color_scheme_expanded: None,
|
|
|
|
|
color_scheme_renaming: None,
|
|
|
|
|
color_scheme_rename_id: widget::Id::unique(),
|
2026-05-24 10:27:32 +02:00
|
|
|
rename_tab: None,
|
|
|
|
|
rename_tab_id: widget::Id::unique(),
|
2026-07-09 14:18:02 +02:00
|
|
|
layout_load_pending: None,
|
|
|
|
|
ssh_hosts: ssh_config_hosts(),
|
2024-02-22 11:18:13 -07:00
|
|
|
color_scheme_tab_model: widget::segmented_button::Model::default(),
|
2024-02-09 15:45:46 -07:00
|
|
|
profile_expanded: None,
|
2024-01-10 01:38:13 +03:00
|
|
|
show_advanced_font_settings: false,
|
2025-12-29 20:42:52 -08:00
|
|
|
shortcut_capture: None,
|
2026-02-03 22:08:34 -08:00
|
|
|
shortcut_conflict: None,
|
|
|
|
|
shortcut_conflict_overlay_restore: None,
|
2026-02-05 11:25:53 -07:00
|
|
|
shortcut_search_focus: Cell::new(true),
|
|
|
|
|
shortcut_search_id: widget::Id::unique(),
|
|
|
|
|
shortcut_search_regex: None,
|
|
|
|
|
shortcut_search_value: String::new(),
|
2024-01-11 13:20:16 -07:00
|
|
|
modifiers: Modifiers::empty(),
|
2024-02-09 09:35:24 +01:00
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
password_mgr: Default::default(),
|
2023-12-17 22:51:50 -07:00
|
|
|
};
|
2023-12-20 13:31:10 -07:00
|
|
|
|
2024-01-10 01:19:04 +03:00
|
|
|
app.set_curr_font_weights_and_stretches();
|
2024-10-22 12:31:02 -06:00
|
|
|
let command = Task::batch([app.update_config(), app.update_title(None)]);
|
2023-12-20 13:31:10 -07:00
|
|
|
|
|
|
|
|
(app, command)
|
2023-12-17 22:51:50 -07:00
|
|
|
}
|
|
|
|
|
|
2024-01-11 13:20:16 -07:00
|
|
|
//TODO: currently the first escape unfocuses, and the second calls this function
|
2024-10-22 12:31:02 -06:00
|
|
|
fn on_escape(&mut self) -> Task<Message> {
|
2024-01-11 13:20:16 -07:00
|
|
|
if self.core.window.show_context {
|
2026-02-05 10:34:58 -07:00
|
|
|
// Handle keyboard shortcut page escape
|
|
|
|
|
if let ContextPage::KeyboardShortcuts = self.context_page {
|
|
|
|
|
// Cancel shortcut capture
|
|
|
|
|
if self.shortcut_capture.take().is_some() {
|
|
|
|
|
return Task::none();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cancel shortcut conflict dialog
|
|
|
|
|
if self.shortcut_conflict.take().is_some() {
|
|
|
|
|
return Task::none();
|
|
|
|
|
}
|
2024-02-09 09:35:24 +01:00
|
|
|
}
|
2026-02-05 10:34:58 -07:00
|
|
|
|
|
|
|
|
return self.update(Message::ToggleContextPage(self.context_page));
|
2024-01-11 13:20:16 -07:00
|
|
|
} else if self.find {
|
|
|
|
|
// Close find if open
|
|
|
|
|
self.find = false;
|
2024-02-02 15:16:43 +01:00
|
|
|
self.find_search_value.clear();
|
2024-01-11 13:20:16 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Focus correct widget
|
|
|
|
|
self.update_focus()
|
|
|
|
|
}
|
|
|
|
|
|
2024-10-22 12:31:02 -06:00
|
|
|
fn on_context_drawer(&mut self) -> Task<Message> {
|
2024-04-15 20:49:03 +01:00
|
|
|
if self.core.window.show_context {
|
2024-10-22 12:31:02 -06:00
|
|
|
Task::none()
|
2024-04-15 20:49:03 +01:00
|
|
|
} else {
|
2024-02-09 09:35:24 +01:00
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
if self.context_page == ContextPage::PasswordManager {
|
|
|
|
|
self.password_mgr.clear();
|
|
|
|
|
}
|
2024-04-15 20:49:03 +01:00
|
|
|
self.update_focus()
|
2024-01-17 16:48:15 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
/// Handle application events here.
|
2024-10-22 12:31:02 -06:00
|
|
|
fn update(&mut self, message: Self::Message) -> Task<Self::Message> {
|
2024-02-12 12:36:30 -07:00
|
|
|
// Helper for updating config values efficiently
|
|
|
|
|
macro_rules! config_set {
|
|
|
|
|
($name: ident, $value: expr) => {
|
|
|
|
|
match &self.config_handler {
|
|
|
|
|
Some(config_handler) => {
|
2024-04-15 21:04:46 +01:00
|
|
|
if let Err(err) =
|
|
|
|
|
paste::paste! { self.config.[<set_ $name>](config_handler, $value) }
|
|
|
|
|
{
|
|
|
|
|
log::warn!("failed to save config {:?}: {}", stringify!($name), err);
|
2024-02-12 12:36:30 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
self.config.$name = $value;
|
|
|
|
|
log::warn!(
|
|
|
|
|
"failed to save config {:?}: no config handler",
|
|
|
|
|
stringify!($name)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
match message {
|
2023-12-22 14:31:01 -07:00
|
|
|
Message::AppTheme(app_theme) => {
|
2024-08-20 07:26:46 -06:00
|
|
|
config_set!(app_theme, app_theme);
|
|
|
|
|
return self.update_config();
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
2024-09-15 23:53:13 +02:00
|
|
|
Message::ClearScrollback(entity_opt) => {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
let mut term = terminal.term.lock();
|
|
|
|
|
term.grid_mut().clear_history();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-22 11:18:13 -07:00
|
|
|
Message::ColorSchemeCollapse => {
|
2024-02-22 09:55:46 -07:00
|
|
|
self.color_scheme_expanded = None;
|
|
|
|
|
}
|
2024-02-22 11:18:13 -07:00
|
|
|
Message::ColorSchemeDelete(color_scheme_kind, color_scheme_id) => {
|
2024-02-22 09:55:46 -07:00
|
|
|
self.color_scheme_expanded = None;
|
2024-02-22 11:18:13 -07:00
|
|
|
self.config
|
|
|
|
|
.color_schemes_mut(color_scheme_kind)
|
|
|
|
|
.remove(&color_scheme_id);
|
|
|
|
|
return self.save_color_schemes(color_scheme_kind);
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
2024-06-05 08:51:26 -06:00
|
|
|
Message::ColorSchemeExport(color_scheme_kind, color_scheme_id_opt) => {
|
2024-02-22 09:55:46 -07:00
|
|
|
self.color_scheme_expanded = None;
|
2024-06-05 08:51:26 -06:00
|
|
|
if let Some(color_scheme_name) = match color_scheme_id_opt {
|
|
|
|
|
Some(color_scheme_id) => self
|
|
|
|
|
.config
|
|
|
|
|
.color_schemes(color_scheme_kind)
|
|
|
|
|
.get(&color_scheme_id)
|
|
|
|
|
.map(|color_scheme| color_scheme.name.clone()),
|
|
|
|
|
None => Some(format!("COSMIC {:?}", color_scheme_kind)),
|
2026-04-10 18:32:09 +02:00
|
|
|
} && self.dialog_opt.is_none()
|
|
|
|
|
{
|
|
|
|
|
let (dialog, command) = Dialog::new(
|
|
|
|
|
DialogSettings::new().kind(DialogKind::SaveFile {
|
|
|
|
|
filename: format!("{}.ron", color_scheme_name),
|
|
|
|
|
}),
|
|
|
|
|
|msg| Message::DialogMessage(Box::new(msg)),
|
|
|
|
|
move |result| {
|
|
|
|
|
Message::ColorSchemeExportResult(
|
|
|
|
|
color_scheme_kind,
|
|
|
|
|
color_scheme_id_opt,
|
|
|
|
|
result,
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
self.dialog_opt = Some(dialog);
|
|
|
|
|
return command;
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-06-05 08:51:26 -06:00
|
|
|
Message::ColorSchemeExportResult(color_scheme_kind, color_scheme_id_opt, result) => {
|
2024-02-22 09:55:46 -07:00
|
|
|
//TODO: show errors in UI
|
|
|
|
|
self.dialog_opt = None;
|
|
|
|
|
if let DialogResult::Open(paths) = result {
|
|
|
|
|
let path = &paths[0];
|
2024-06-05 08:51:26 -06:00
|
|
|
match color_scheme_id_opt {
|
|
|
|
|
Some(color_scheme_id) => {
|
|
|
|
|
if let Some(color_scheme) = self
|
|
|
|
|
.config
|
|
|
|
|
.color_schemes(color_scheme_kind)
|
|
|
|
|
.get(&color_scheme_id)
|
|
|
|
|
{
|
|
|
|
|
match ron::ser::to_string_pretty(
|
|
|
|
|
&color_scheme,
|
|
|
|
|
ron::ser::PrettyConfig::new(),
|
|
|
|
|
) {
|
|
|
|
|
Ok(ron) => {
|
|
|
|
|
if let Err(err) = fs::write(path, ron) {
|
|
|
|
|
log::error!(
|
|
|
|
|
"failed to export {:?} to {:?}: {}",
|
|
|
|
|
color_scheme_id,
|
|
|
|
|
path,
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::error!(
|
|
|
|
|
"failed to serialize color scheme {:?}: {}",
|
|
|
|
|
color_scheme_id,
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
log::error!("failed to find color scheme {:?}", color_scheme_id);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
let name = format!("COSMIC {:?}", color_scheme_kind);
|
|
|
|
|
let color_scheme = match color_scheme_kind {
|
|
|
|
|
ColorSchemeKind::Dark => ColorScheme::from((
|
|
|
|
|
name.as_str(),
|
|
|
|
|
&terminal_theme::cosmic_dark(),
|
|
|
|
|
)),
|
|
|
|
|
ColorSchemeKind::Light => ColorScheme::from((
|
|
|
|
|
name.as_str(),
|
|
|
|
|
&terminal_theme::cosmic_light(),
|
|
|
|
|
)),
|
|
|
|
|
};
|
|
|
|
|
//TODO: do not duplicate code
|
|
|
|
|
match ron::ser::to_string_pretty(
|
|
|
|
|
&color_scheme,
|
|
|
|
|
ron::ser::PrettyConfig::new(),
|
|
|
|
|
) {
|
|
|
|
|
Ok(ron) => {
|
|
|
|
|
if let Err(err) = fs::write(path, ron) {
|
|
|
|
|
log::error!(
|
|
|
|
|
"failed to export {:?} to {:?}: {}",
|
|
|
|
|
color_scheme.name,
|
|
|
|
|
path,
|
|
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
2024-02-22 09:55:46 -07:00
|
|
|
log::error!(
|
2024-06-05 08:51:26 -06:00
|
|
|
"failed to serialize color scheme {:?}: {}",
|
|
|
|
|
color_scheme.name,
|
2024-02-22 09:55:46 -07:00
|
|
|
err
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-04-15 20:52:07 +01:00
|
|
|
}
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-06-05 08:51:26 -06:00
|
|
|
Message::ColorSchemeExpand(color_scheme_kind, color_scheme_id_opt) => {
|
|
|
|
|
self.color_scheme_expanded = Some((color_scheme_kind, color_scheme_id_opt));
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
2024-02-22 11:18:13 -07:00
|
|
|
Message::ColorSchemeImport(color_scheme_kind) => {
|
2024-02-22 09:55:46 -07:00
|
|
|
if self.dialog_opt.is_none() {
|
|
|
|
|
self.color_scheme_errors.clear();
|
|
|
|
|
let (dialog, command) = Dialog::new(
|
2025-07-01 09:41:36 -06:00
|
|
|
DialogSettings::new().kind(DialogKind::OpenMultipleFiles),
|
2026-04-10 18:32:09 +02:00
|
|
|
|msg| Message::DialogMessage(Box::new(msg)),
|
2024-02-22 11:18:13 -07:00
|
|
|
move |result| Message::ColorSchemeImportResult(color_scheme_kind, result),
|
2024-02-22 09:55:46 -07:00
|
|
|
);
|
|
|
|
|
self.dialog_opt = Some(dialog);
|
|
|
|
|
return command;
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-22 11:18:13 -07:00
|
|
|
Message::ColorSchemeImportResult(color_scheme_kind, result) => {
|
2024-02-22 09:55:46 -07:00
|
|
|
self.dialog_opt = None;
|
|
|
|
|
if let DialogResult::Open(paths) = result {
|
|
|
|
|
self.color_scheme_errors.clear();
|
2024-04-14 21:09:14 +01:00
|
|
|
for path in &paths {
|
2024-02-22 09:55:46 -07:00
|
|
|
let mut file = match fs::File::open(path) {
|
|
|
|
|
Ok(ok) => ok,
|
|
|
|
|
Err(err) => {
|
|
|
|
|
self.color_scheme_errors
|
2024-04-14 20:53:23 +01:00
|
|
|
.push(format!("Failed to open {path:?}: {err}"));
|
2024-02-22 09:55:46 -07:00
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
match ron::de::from_reader::<_, ColorScheme>(&mut file) {
|
|
|
|
|
Ok(color_scheme) => {
|
|
|
|
|
// Get next color_scheme ID
|
|
|
|
|
let color_scheme_id = self
|
|
|
|
|
.config
|
2024-02-22 11:18:13 -07:00
|
|
|
.color_schemes(color_scheme_kind)
|
2024-02-22 09:55:46 -07:00
|
|
|
.last_key_value()
|
|
|
|
|
.map(|(id, _)| ColorSchemeId(id.0 + 1))
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
self.config
|
2024-02-22 11:18:13 -07:00
|
|
|
.color_schemes_mut(color_scheme_kind)
|
2024-02-22 09:55:46 -07:00
|
|
|
.insert(color_scheme_id, color_scheme);
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
self.color_scheme_errors
|
2024-04-14 20:53:23 +01:00
|
|
|
.push(format!("Failed to parse {path:?}: {err}"));
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-22 11:18:13 -07:00
|
|
|
return self.save_color_schemes(color_scheme_kind);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::ColorSchemeRename(color_scheme_kind, color_scheme_id, color_scheme_name) => {
|
|
|
|
|
self.color_scheme_expanded = None;
|
|
|
|
|
let focus = self.color_scheme_renaming.is_none();
|
|
|
|
|
self.color_scheme_renaming =
|
|
|
|
|
Some((color_scheme_kind, color_scheme_id, color_scheme_name));
|
|
|
|
|
if focus {
|
|
|
|
|
return widget::text_input::focus(self.color_scheme_rename_id.clone());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::ColorSchemeRenameSubmit => {
|
|
|
|
|
if let Some((color_scheme_kind, color_scheme_id, color_scheme_name)) =
|
|
|
|
|
self.color_scheme_renaming.take()
|
2026-04-10 18:09:25 +02:00
|
|
|
&& let Some(color_scheme) = self
|
2024-02-22 11:18:13 -07:00
|
|
|
.config
|
|
|
|
|
.color_schemes_mut(color_scheme_kind)
|
|
|
|
|
.get_mut(&color_scheme_id)
|
2026-04-10 18:32:09 +02:00
|
|
|
{
|
|
|
|
|
color_scheme.name = color_scheme_name;
|
|
|
|
|
return self.save_color_schemes(color_scheme_kind);
|
2024-02-22 11:18:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::ColorSchemeTabActivate(entity) => {
|
|
|
|
|
if let Some(color_scheme_kind) =
|
|
|
|
|
self.color_scheme_tab_model.data::<ColorSchemeKind>(entity)
|
|
|
|
|
{
|
|
|
|
|
let context_page = ContextPage::ColorSchemes(*color_scheme_kind);
|
|
|
|
|
if self.context_page != context_page {
|
|
|
|
|
return self.update(Message::ToggleContextPage(context_page));
|
|
|
|
|
}
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-22 14:31:01 -07:00
|
|
|
Message::Config(config) => {
|
2026-04-10 19:00:25 +02:00
|
|
|
if *config != self.config {
|
2026-02-05 10:34:58 -07:00
|
|
|
let shortcuts_changed = config.shortcuts_custom != self.config.shortcuts_custom;
|
2023-12-22 14:31:01 -07:00
|
|
|
log::info!("update config");
|
|
|
|
|
//TODO: update syntax theme by clearing tabs, only if needed
|
2026-04-10 19:00:25 +02:00
|
|
|
self.config = *config;
|
2026-01-29 20:10:25 -08:00
|
|
|
if shortcuts_changed {
|
2026-02-05 12:57:56 -07:00
|
|
|
self.shortcuts_config =
|
|
|
|
|
shortcuts::ShortcutsConfig::new(self.config.shortcuts_custom.clone());
|
2026-01-29 20:10:25 -08:00
|
|
|
self.key_binds = key_binds(&self.shortcuts_config);
|
|
|
|
|
}
|
2023-12-22 14:31:01 -07:00
|
|
|
return self.update_config();
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-22 15:00:50 -07:00
|
|
|
Message::Copy(entity_opt) => {
|
2024-01-17 16:09:56 +01:00
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
let term = terminal.term.lock();
|
|
|
|
|
if let Some(text) = term.selection_to_string() {
|
2024-10-22 12:31:02 -06:00
|
|
|
return Task::batch([clipboard::write(text), self.update_focus()]);
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
2023-12-21 22:13:17 -07:00
|
|
|
}
|
2024-01-17 16:09:56 +01:00
|
|
|
} else {
|
|
|
|
|
log::warn!("Failed to get focused pane");
|
2024-05-03 14:37:01 -07:00
|
|
|
}
|
|
|
|
|
return self.update_focus();
|
|
|
|
|
}
|
|
|
|
|
Message::CopyOrSigint(entity_opt) => {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
2025-03-13 15:05:59 -06:00
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
let mut term = terminal.term.lock();
|
2024-05-03 14:37:01 -07:00
|
|
|
if let Some(text) = term.selection_to_string() {
|
2025-03-13 15:05:59 -06:00
|
|
|
// Clear selection (to allow next Ctrl+C to signal)
|
|
|
|
|
term.selection = None;
|
|
|
|
|
drop(term);
|
|
|
|
|
// Mark as dirty
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
drop(terminal);
|
2024-10-22 12:31:02 -06:00
|
|
|
return Task::batch([clipboard::write(text), self.update_focus()]);
|
2024-05-03 14:37:01 -07:00
|
|
|
} else {
|
|
|
|
|
// Drop the lock for term so that input_scroll doesn't block forever
|
|
|
|
|
drop(term);
|
|
|
|
|
// 0x03 is ^C
|
2024-05-14 13:15:02 -06:00
|
|
|
terminal.input_scroll(b"\x03".as_slice());
|
2024-05-03 14:37:01 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
log::warn!("Failed to get focused pane");
|
2023-12-21 22:13:17 -07:00
|
|
|
}
|
2024-02-05 19:35:03 +01:00
|
|
|
return self.update_focus();
|
2023-12-21 22:13:17 -07:00
|
|
|
}
|
2025-06-19 06:38:57 +02:00
|
|
|
Message::ToggleFullscreen => {
|
|
|
|
|
if let Some(window_id) = self.core.main_window_id() {
|
|
|
|
|
return cosmic::command::toggle_maximize(window_id);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-21 21:13:13 +01:00
|
|
|
Message::CopyPrimary(entity_opt) => {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
let term = terminal.term.lock();
|
|
|
|
|
if let Some(text) = term.selection_to_string() {
|
2024-10-22 12:31:02 -06:00
|
|
|
return Task::batch([
|
2024-04-26 10:24:07 -06:00
|
|
|
clipboard::write_primary(text),
|
|
|
|
|
self.update_focus(),
|
|
|
|
|
]);
|
2024-01-21 21:13:13 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
log::warn!("Failed to get focused pane");
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-22 14:31:01 -07:00
|
|
|
Message::DefaultFont(index) => {
|
|
|
|
|
match self.font_names.get(index) {
|
|
|
|
|
Some(font_name) => {
|
|
|
|
|
if font_name != &self.config.font_name {
|
|
|
|
|
// Update font name from config
|
|
|
|
|
{
|
|
|
|
|
let mut font_system = font_system().write().unwrap();
|
|
|
|
|
font_system.raw().db_mut().set_monospace_family(font_name);
|
|
|
|
|
}
|
2024-01-17 16:09:56 +01:00
|
|
|
let panes: Vec<_> = self.pane_model.panes.iter().collect();
|
|
|
|
|
for (_pane, tab_model) in panes {
|
|
|
|
|
let entities: Vec<_> = tab_model.iter().collect();
|
|
|
|
|
for entity in entities {
|
|
|
|
|
if let Some(terminal) =
|
|
|
|
|
tab_model.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
{
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.update_cell_size();
|
|
|
|
|
}
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-20 07:26:46 -06:00
|
|
|
config_set!(font_name, font_name.to_string());
|
2024-01-10 01:19:04 +03:00
|
|
|
self.set_curr_font_weights_and_stretches();
|
|
|
|
|
|
2024-08-20 07:26:46 -06:00
|
|
|
return self.update_config();
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
log::warn!("failed to find font with index {}", index);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-09 10:16:32 -07:00
|
|
|
Message::DefaultFontSize(index) => match self.font_sizes.get(index) {
|
|
|
|
|
Some(font_size) => {
|
2024-08-20 07:26:46 -06:00
|
|
|
config_set!(font_size, *font_size);
|
2024-08-20 04:52:34 +03:00
|
|
|
self.reset_terminal_panes_zoom(); // reset zoom
|
2024-08-20 07:26:46 -06:00
|
|
|
return self.update_config();
|
2024-01-01 12:27:51 +03:00
|
|
|
}
|
|
|
|
|
None => {
|
2024-01-09 10:16:32 -07:00
|
|
|
log::warn!("failed to find font with index {}", index);
|
2024-01-01 12:27:51 +03:00
|
|
|
}
|
|
|
|
|
},
|
2024-01-10 01:19:04 +03:00
|
|
|
Message::DefaultFontStretch(index) => match self.curr_font_stretches.get(index) {
|
|
|
|
|
Some(font_stretch) => {
|
2024-08-20 07:26:46 -06:00
|
|
|
config_set!(font_stretch, font_stretch.to_number());
|
2024-01-10 01:19:04 +03:00
|
|
|
self.set_curr_font_weights_and_stretches();
|
2024-08-20 07:26:46 -06:00
|
|
|
return self.update_config();
|
2024-01-10 01:19:04 +03:00
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
log::warn!("failed to find font weight with index {}", index);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Message::DefaultFontWeight(index) => match self.curr_font_weights.get(index) {
|
|
|
|
|
Some(font_weight) => {
|
2024-08-20 07:26:46 -06:00
|
|
|
config_set!(font_weight, *font_weight);
|
|
|
|
|
return self.update_config();
|
2024-01-10 01:19:04 +03:00
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
log::warn!("failed to find font weight with index {}", index);
|
|
|
|
|
}
|
|
|
|
|
},
|
2024-01-12 01:58:57 +03:00
|
|
|
Message::DefaultDimFontWeight(index) => match self.curr_font_weights.get(index) {
|
|
|
|
|
Some(font_weight) => {
|
2024-08-20 07:26:46 -06:00
|
|
|
config_set!(dim_font_weight, *font_weight);
|
|
|
|
|
return self.update_config();
|
2024-01-12 01:58:57 +03:00
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
log::warn!("failed to find dim font weight with index {}", index);
|
|
|
|
|
}
|
|
|
|
|
},
|
2024-01-10 01:19:04 +03:00
|
|
|
Message::DefaultBoldFontWeight(index) => match self.curr_font_weights.get(index) {
|
|
|
|
|
Some(font_weight) => {
|
2024-08-20 07:26:46 -06:00
|
|
|
config_set!(bold_font_weight, *font_weight);
|
|
|
|
|
return self.update_config();
|
2024-01-10 01:19:04 +03:00
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
log::warn!("failed to find bold font weight with index {}", index);
|
|
|
|
|
}
|
|
|
|
|
},
|
2024-01-09 10:16:32 -07:00
|
|
|
Message::DefaultZoomStep(index) => match self.zoom_steps.get(index) {
|
|
|
|
|
Some(zoom_step) => {
|
2024-08-20 07:26:46 -06:00
|
|
|
config_set!(font_size_zoom_step_mul_100, *zoom_step);
|
2024-08-20 04:52:34 +03:00
|
|
|
self.reset_terminal_panes_zoom(); // reset zoom
|
2024-08-20 07:26:46 -06:00
|
|
|
return self.update_config();
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
None => {
|
2024-01-09 10:16:32 -07:00
|
|
|
log::warn!("failed to find zoom step with index {}", index);
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
},
|
2024-02-22 09:55:46 -07:00
|
|
|
Message::DialogMessage(dialog_message) => {
|
|
|
|
|
if let Some(dialog) = &mut self.dialog_opt {
|
2026-04-10 18:32:09 +02:00
|
|
|
// DialogMessage is boxed, so we need to dereference it before updating
|
|
|
|
|
return dialog.update(*dialog_message);
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-08-03 20:33:55 -06:00
|
|
|
Message::Drop(Some((pane, entity, data))) => {
|
2025-01-21 17:07:45 +01:00
|
|
|
self.pane_model.set_focus(pane);
|
2024-08-03 20:33:55 -06:00
|
|
|
if let Ok(value) = shlex::try_join(data.paths.iter().filter_map(|p| p.to_str())) {
|
2024-10-22 12:31:02 -06:00
|
|
|
return Task::batch([
|
2024-08-03 20:33:55 -06:00
|
|
|
self.update_focus(),
|
2025-03-19 12:27:00 -04:00
|
|
|
cosmic::task::message(action::app(Message::PasteValue(
|
|
|
|
|
Some(entity),
|
|
|
|
|
value,
|
|
|
|
|
))),
|
2024-08-03 20:33:55 -06:00
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::Drop(None) => {}
|
2024-01-11 13:20:16 -07:00
|
|
|
Message::Find(find) => {
|
|
|
|
|
self.find = find;
|
2024-02-02 15:16:43 +01:00
|
|
|
if find {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let entity = tab_model.active();
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
let term = terminal.term.lock();
|
|
|
|
|
if let Some(text) = term.selection_to_string() {
|
|
|
|
|
self.find_search_value = text;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
log::warn!("Failed to get focused pane");
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
self.find_search_value.clear();
|
|
|
|
|
}
|
2024-01-11 13:20:16 -07:00
|
|
|
|
|
|
|
|
// Focus correct input
|
|
|
|
|
return self.update_focus();
|
|
|
|
|
}
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
Message::FindCaseSensitive(case_sensitive) => {
|
|
|
|
|
self.find_case_sensitive = case_sensitive;
|
|
|
|
|
return self.update_focus();
|
|
|
|
|
}
|
2024-01-11 13:20:16 -07:00
|
|
|
Message::FindNext => {
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
if !self.find_search_value.is_empty() {
|
|
|
|
|
let pattern = self.find_pattern();
|
|
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let entity = tab_model.active();
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.search(&pattern, true);
|
|
|
|
|
}
|
2024-01-11 14:31:20 -07:00
|
|
|
}
|
2024-01-11 13:20:16 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Focus correct input
|
|
|
|
|
return self.update_focus();
|
|
|
|
|
}
|
|
|
|
|
Message::FindPrevious => {
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
if !self.find_search_value.is_empty() {
|
|
|
|
|
let pattern = self.find_pattern();
|
|
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let entity = tab_model.active();
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.search(&pattern, false);
|
|
|
|
|
}
|
2024-01-11 14:31:20 -07:00
|
|
|
}
|
2024-01-11 13:20:16 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Focus correct input
|
|
|
|
|
return self.update_focus();
|
|
|
|
|
}
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
Message::FindRegex(regex) => {
|
|
|
|
|
self.find_regex = regex;
|
|
|
|
|
return self.update_focus();
|
|
|
|
|
}
|
2024-01-11 13:20:16 -07:00
|
|
|
Message::FindSearchValueChanged(value) => {
|
|
|
|
|
self.find_search_value = value;
|
|
|
|
|
}
|
2024-01-21 21:13:13 +01:00
|
|
|
Message::MiddleClick(pane, entity_opt) => {
|
2025-01-21 17:07:45 +01:00
|
|
|
self.pane_model.set_focus(pane);
|
2024-10-22 12:31:02 -06:00
|
|
|
return Task::batch([
|
2024-01-21 21:13:13 +01:00
|
|
|
self.update_focus(),
|
2024-10-22 12:31:02 -06:00
|
|
|
clipboard::read_primary().map(move |value_opt| match value_opt {
|
2025-03-19 12:27:00 -04:00
|
|
|
Some(value) => action::app(Message::PasteValue(entity_opt, value)),
|
|
|
|
|
None => action::none(),
|
2024-01-21 21:13:13 +01:00
|
|
|
}),
|
|
|
|
|
]);
|
|
|
|
|
}
|
2024-02-28 09:15:58 -07:00
|
|
|
Message::FocusFollowMouse(focus_follow_mouse) => {
|
|
|
|
|
config_set!(focus_follow_mouse, focus_follow_mouse);
|
|
|
|
|
}
|
2026-05-26 15:40:59 -04:00
|
|
|
Message::Key(modifiers, physical, key) => {
|
2026-02-05 13:17:18 -07:00
|
|
|
// Hard-coded keys
|
|
|
|
|
match key {
|
|
|
|
|
Key::Named(Named::Copy) => {
|
|
|
|
|
return self.update(Message::Copy(None));
|
2026-02-03 22:08:34 -08:00
|
|
|
}
|
2026-02-05 13:17:18 -07:00
|
|
|
Key::Named(Named::Paste) => {
|
|
|
|
|
return self.update(Message::Paste(None));
|
|
|
|
|
}
|
2026-02-05 13:39:32 -07:00
|
|
|
Key::Named(Named::Escape) => {
|
|
|
|
|
// Handled by on_escape
|
|
|
|
|
return Task::none();
|
|
|
|
|
}
|
2026-02-05 13:17:18 -07:00
|
|
|
_ => {}
|
2026-02-03 22:08:34 -08:00
|
|
|
}
|
2026-02-05 13:17:18 -07:00
|
|
|
|
|
|
|
|
// Handle shortcut capture
|
2025-12-29 20:42:52 -08:00
|
|
|
if let Some(action) = self.shortcut_capture {
|
|
|
|
|
if let Some(binding) = shortcuts::binding_from_key(modifiers, key) {
|
|
|
|
|
self.shortcut_capture = None;
|
2026-02-03 22:08:34 -08:00
|
|
|
if let Some(existing_action) =
|
|
|
|
|
self.shortcuts_config.action_for_binding(&binding)
|
|
|
|
|
{
|
|
|
|
|
if existing_action != action {
|
|
|
|
|
self.begin_shortcut_conflict(ShortcutConflict {
|
|
|
|
|
binding,
|
|
|
|
|
existing_action,
|
|
|
|
|
new_action: action,
|
|
|
|
|
});
|
|
|
|
|
return Task::none();
|
|
|
|
|
}
|
|
|
|
|
return Task::none();
|
|
|
|
|
}
|
|
|
|
|
self.apply_shortcut_binding(binding, action);
|
2025-12-29 20:42:52 -08:00
|
|
|
}
|
|
|
|
|
return Task::none();
|
|
|
|
|
}
|
2026-02-05 13:17:18 -07:00
|
|
|
|
|
|
|
|
// Handle configurable keys
|
2024-04-14 21:09:14 +01:00
|
|
|
for (key_bind, action) in &self.key_binds {
|
2026-05-26 15:40:59 -04:00
|
|
|
if key_bind.matches(modifiers, &key, Some(&physical)) {
|
2024-02-28 09:15:58 -07:00
|
|
|
return self.update(action.message(None));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-15 21:04:46 +01:00
|
|
|
Message::LaunchUrl(url) => {
|
|
|
|
|
if let Err(err) = open::that_detached(&url) {
|
2024-02-28 09:15:58 -07:00
|
|
|
log::warn!("failed to open {:?}: {}", url, err);
|
|
|
|
|
}
|
2024-04-15 21:04:46 +01:00
|
|
|
}
|
2026-02-15 10:23:49 +01:00
|
|
|
Message::CopyUrlByMenu => {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let entity = tab_model.active();
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
2026-02-28 22:04:01 +01:00
|
|
|
let mut terminal = terminal.lock().unwrap();
|
2026-02-15 10:23:49 +01:00
|
|
|
if let Some(url) =
|
|
|
|
|
terminal.context_menu.as_ref().and_then(|m| m.link.as_ref())
|
|
|
|
|
{
|
2026-02-28 22:04:01 +01:00
|
|
|
let url = url.to_owned();
|
|
|
|
|
terminal.context_menu = None;
|
|
|
|
|
terminal.active_regex_match = None;
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
|
|
|
|
|
return Task::batch([clipboard::write(url), self.update_focus()]);
|
2026-02-15 10:23:49 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-16 09:27:00 +02:00
|
|
|
Message::LaunchUrlByMenu => {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let entity = tab_model.active();
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
if let Some(url) =
|
|
|
|
|
terminal.context_menu.as_ref().and_then(|m| m.link.as_ref())
|
2026-04-10 18:32:09 +02:00
|
|
|
&& let Err(err) = open::that_detached(url)
|
2025-10-16 09:27:00 +02:00
|
|
|
{
|
2026-04-10 18:32:09 +02:00
|
|
|
log::warn!("failed to open {:?}: {}", url, err);
|
2025-10-16 09:27:00 +02:00
|
|
|
}
|
|
|
|
|
terminal.context_menu = None;
|
2026-02-28 22:04:01 +01:00
|
|
|
terminal.active_regex_match = None;
|
|
|
|
|
terminal.needs_update = true;
|
2025-10-16 09:27:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-07-09 14:18:02 +02:00
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-11 13:20:16 -07:00
|
|
|
Message::Modifiers(modifiers) => {
|
|
|
|
|
self.modifiers = modifiers;
|
|
|
|
|
}
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
Message::MonitorTick => {
|
|
|
|
|
let panes: Vec<pane_grid::Pane> = self
|
|
|
|
|
.pane_model
|
|
|
|
|
.panes
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|(pane, _)| *pane)
|
|
|
|
|
.collect();
|
|
|
|
|
for pane in panes {
|
|
|
|
|
let Some(tab_model) = self.pane_model.panes.get_mut(pane) else {
|
|
|
|
|
continue;
|
|
|
|
|
};
|
|
|
|
|
let entities: Vec<segmented_button::Entity> = tab_model.iter().collect();
|
|
|
|
|
for entity in entities {
|
|
|
|
|
let mut icon_update = None;
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
|
|
|
|
|
// Silence: alert when no output for the configured delay
|
|
|
|
|
if terminal.monitor_silence
|
|
|
|
|
&& terminal.monitor_alert.is_none()
|
|
|
|
|
&& terminal.monitor_last_output.elapsed().as_secs()
|
|
|
|
|
>= MONITOR_SILENCE_SECONDS
|
|
|
|
|
{
|
|
|
|
|
terminal.monitor_alert = Some(MonitorKind::Silence);
|
|
|
|
|
icon_update = Some(monitor_icon_name(&terminal));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Process finished: alert when the foreground job goes away
|
|
|
|
|
if terminal.monitor_process_finished {
|
|
|
|
|
let job_running =
|
|
|
|
|
terminal.foreground_job_running().unwrap_or(false);
|
|
|
|
|
if terminal.monitor_job_running && !job_running {
|
|
|
|
|
terminal.monitor_alert = Some(MonitorKind::ProcessFinished);
|
|
|
|
|
icon_update = Some(monitor_icon_name(&terminal));
|
|
|
|
|
}
|
|
|
|
|
terminal.monitor_job_running = job_running;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(icon_name) = icon_update {
|
|
|
|
|
monitor_update_tab_icon(tab_model, entity, icon_name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)
Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.
- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
plus la vue en bas. Couvre touches legacy et kitty, collage, clic
milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
application attendrait une reponse qui ne vient pas (CSI ? u de Claude
Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
(selection, copie et defilement locaux, meme dans une application qui
la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
action ToggleReadOnly dans les raccourcis (groupe Autre, sans
raccourci par defaut, comme Ghostty et Konsole) ; cadenas
changes-prevent-symbolic sur l'onglet, apres les alertes.
Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:19:02 +02:00
|
|
|
Message::ReadOnlyToggle(entity_opt) => {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.active_mut() {
|
|
|
|
|
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
|
|
|
|
|
let mut icon_update = None;
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.read_only = !terminal.read_only;
|
|
|
|
|
icon_update = Some(monitor_icon_name(&terminal));
|
|
|
|
|
}
|
|
|
|
|
if let Some(icon_name) = icon_update {
|
|
|
|
|
monitor_update_tab_icon(tab_model, entity, icon_name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
Message::MonitorToggle(monitor_kind, entity_opt) => {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.active_mut() {
|
|
|
|
|
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
|
|
|
|
|
let mut icon_update = None;
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
match monitor_kind {
|
|
|
|
|
MonitorKind::Activity => {
|
|
|
|
|
terminal.monitor_activity = !terminal.monitor_activity;
|
|
|
|
|
}
|
|
|
|
|
MonitorKind::Silence => {
|
|
|
|
|
terminal.monitor_silence = !terminal.monitor_silence;
|
|
|
|
|
terminal.monitor_last_output = std::time::Instant::now();
|
|
|
|
|
}
|
|
|
|
|
MonitorKind::ProcessFinished => {
|
|
|
|
|
terminal.monitor_process_finished =
|
|
|
|
|
!terminal.monitor_process_finished;
|
|
|
|
|
terminal.monitor_job_running =
|
|
|
|
|
terminal.foreground_job_running().unwrap_or(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
terminal.monitor_alert = None;
|
|
|
|
|
icon_update = Some(monitor_icon_name(&terminal));
|
|
|
|
|
}
|
|
|
|
|
if let Some(icon_name) = icon_update {
|
|
|
|
|
monitor_update_tab_icon(tab_model, entity, icon_name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-22 16:35:23 +01:00
|
|
|
Message::MouseEnter(pane) => {
|
2025-01-21 17:07:45 +01:00
|
|
|
self.pane_model.set_focus(pane);
|
2024-01-22 16:35:23 +01:00
|
|
|
return self.update_focus();
|
|
|
|
|
}
|
2025-12-29 20:42:52 -08:00
|
|
|
Message::ShortcutCaptureCancel => {
|
|
|
|
|
self.shortcut_capture = None;
|
|
|
|
|
}
|
|
|
|
|
Message::ShortcutCaptureStart(action) => {
|
|
|
|
|
self.shortcut_capture = Some(action);
|
|
|
|
|
}
|
2026-02-03 22:08:34 -08:00
|
|
|
Message::ShortcutConflictCancel => {
|
|
|
|
|
self.clear_shortcut_conflict();
|
|
|
|
|
}
|
|
|
|
|
Message::ShortcutConflictReplace => {
|
|
|
|
|
if let Some(conflict) = self.shortcut_conflict.clone() {
|
|
|
|
|
self.apply_shortcut_binding(conflict.binding, conflict.new_action);
|
|
|
|
|
}
|
|
|
|
|
self.clear_shortcut_conflict();
|
|
|
|
|
}
|
2025-12-29 20:42:52 -08:00
|
|
|
Message::ShortcutRemove(binding, source) => {
|
|
|
|
|
match source {
|
|
|
|
|
shortcuts::BindingSource::Default => {
|
|
|
|
|
self.shortcuts_config
|
|
|
|
|
.custom
|
|
|
|
|
.0
|
2026-02-05 13:49:05 -07:00
|
|
|
.insert(binding, shortcuts::KeyBindAction::Disable);
|
2025-12-29 20:42:52 -08:00
|
|
|
}
|
|
|
|
|
shortcuts::BindingSource::Custom => {
|
|
|
|
|
self.shortcuts_config.custom.0.remove(&binding);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.save_shortcuts_custom();
|
|
|
|
|
}
|
2026-02-05 12:57:56 -07:00
|
|
|
Message::ShortcutReset(reset_action) => {
|
|
|
|
|
self.shortcuts_config.reset_action(reset_action);
|
|
|
|
|
self.save_shortcuts_custom();
|
|
|
|
|
}
|
2026-02-05 11:25:53 -07:00
|
|
|
Message::ShortcutSearch(search) => {
|
|
|
|
|
self.shortcut_search_focus.set(true);
|
|
|
|
|
self.shortcut_search_regex = None;
|
|
|
|
|
if !search.is_empty() {
|
|
|
|
|
let pattern = regex::escape(&search);
|
|
|
|
|
match regex::RegexBuilder::new(&pattern)
|
|
|
|
|
.case_insensitive(true)
|
|
|
|
|
.build()
|
|
|
|
|
{
|
|
|
|
|
Ok(regex) => {
|
|
|
|
|
self.shortcut_search_regex = Some(regex);
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::warn!("failed to parse regex {:?}: {}", pattern, err);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
self.shortcut_search_value = search;
|
|
|
|
|
return self.update_focus();
|
|
|
|
|
}
|
2024-02-12 12:36:30 -07:00
|
|
|
Message::Opacity(opacity) => {
|
|
|
|
|
config_set!(opacity, cmp::min(100, opacity));
|
|
|
|
|
}
|
2024-01-17 16:09:56 +01:00
|
|
|
Message::PaneClicked(pane) => {
|
2025-01-21 17:07:45 +01:00
|
|
|
self.pane_model.set_focus(pane);
|
2026-07-09 13:51:51 +02:00
|
|
|
let title_task = self.update_title(Some(pane));
|
|
|
|
|
return Task::batch([self.update_focus(), title_task]);
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
|
|
|
|
Message::PaneSplit(axis) => {
|
|
|
|
|
let result = self.pane_model.panes.split(
|
|
|
|
|
axis,
|
2025-01-21 17:07:45 +01:00
|
|
|
self.pane_model.focused(),
|
2024-01-17 16:09:56 +01:00
|
|
|
segmented_button::ModelBuilder::default().build(),
|
|
|
|
|
);
|
|
|
|
|
if let Some((pane, _)) = result {
|
|
|
|
|
self.terminal_ids.insert(pane, widget::Id::unique());
|
2024-02-26 04:47:52 +01:00
|
|
|
let command =
|
2026-03-06 13:53:46 +01:00
|
|
|
self.create_and_focus_new_terminal(pane, self.get_default_profile(), false);
|
2024-01-17 16:09:56 +01:00
|
|
|
self.pane_model.panes_created += 1;
|
2024-02-09 15:45:46 -07:00
|
|
|
return command;
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::PaneToggleMaximized => {
|
|
|
|
|
if self.pane_model.panes.maximized().is_some() {
|
|
|
|
|
self.pane_model.panes.restore();
|
|
|
|
|
} else {
|
2025-01-21 17:07:45 +01:00
|
|
|
self.pane_model.panes.maximize(self.pane_model.focused());
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
|
|
|
|
return self.update_focus();
|
|
|
|
|
}
|
|
|
|
|
Message::PaneFocusAdjacent(direction) => {
|
|
|
|
|
if let Some(adjacent) = self
|
|
|
|
|
.pane_model
|
|
|
|
|
.panes
|
2025-01-21 17:07:45 +01:00
|
|
|
.adjacent(self.pane_model.focused(), direction)
|
2024-01-17 16:09:56 +01:00
|
|
|
{
|
2025-01-21 17:07:45 +01:00
|
|
|
self.pane_model.set_focus(adjacent);
|
2024-01-18 10:15:16 +01:00
|
|
|
return self.update_title(Some(adjacent));
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::PaneResized(pane_grid::ResizeEvent { split, ratio }) => {
|
|
|
|
|
self.pane_model.panes.resize(split, ratio);
|
|
|
|
|
}
|
|
|
|
|
Message::PaneDragged(pane_grid::DragEvent::Dropped { pane, target }) => {
|
|
|
|
|
self.pane_model.panes.drop(pane, target);
|
|
|
|
|
}
|
|
|
|
|
Message::PaneDragged(_) => {}
|
2024-02-09 09:35:24 +01:00
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
Message::PasswordManager(msg) => {
|
|
|
|
|
return self.password_mgr.update(msg);
|
|
|
|
|
}
|
|
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
Message::PasswordPaste(password, pane) => {
|
|
|
|
|
if 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) {
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.paste(password.into_unsecure());
|
|
|
|
|
terminal.input_scroll(b"\n".as_slice());
|
|
|
|
|
self.core.window.show_context = false;
|
|
|
|
|
self.password_mgr.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-22 15:00:50 -07:00
|
|
|
Message::Paste(entity_opt) => {
|
2024-10-22 12:31:02 -06:00
|
|
|
return clipboard::read().map(move |value_opt| match value_opt {
|
2025-03-19 12:27:00 -04:00
|
|
|
Some(value) => action::app(Message::PasteValue(entity_opt, value)),
|
|
|
|
|
None => action::none(),
|
2023-12-21 22:13:17 -07:00
|
|
|
});
|
|
|
|
|
}
|
2024-01-20 14:13:04 +01:00
|
|
|
Message::PastePrimary(entity_opt) => {
|
2024-10-22 12:31:02 -06:00
|
|
|
return clipboard::read_primary().map(move |value_opt| match value_opt {
|
2025-03-19 12:27:00 -04:00
|
|
|
Some(value) => action::app(Message::PasteValue(entity_opt, value)),
|
|
|
|
|
None => action::none(),
|
2024-01-20 14:13:04 +01:00
|
|
|
});
|
|
|
|
|
}
|
2023-12-22 15:00:50 -07:00
|
|
|
Message::PasteValue(entity_opt, value) => {
|
2024-01-17 16:09:56 +01:00
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.paste(value);
|
|
|
|
|
}
|
2023-12-21 22:13:17 -07:00
|
|
|
}
|
2024-02-05 11:28:41 +01:00
|
|
|
return self.update_focus();
|
2023-12-21 22:13:17 -07:00
|
|
|
}
|
2024-02-09 15:45:46 -07:00
|
|
|
Message::ProfileCollapse(_profile_id) => {
|
|
|
|
|
self.profile_expanded = None;
|
|
|
|
|
}
|
|
|
|
|
Message::ProfileCommand(profile_id, text) => {
|
|
|
|
|
if let Some(profile) = self.config.profiles.get_mut(&profile_id) {
|
|
|
|
|
profile.command = text;
|
|
|
|
|
return self.save_profiles();
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-14 21:17:32 +02:00
|
|
|
Message::ProfileDirectory(profile_id, text) => {
|
|
|
|
|
if let Some(profile) = self.config.profiles.get_mut(&profile_id) {
|
|
|
|
|
profile.working_directory = text;
|
|
|
|
|
return self.save_profiles();
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-09 15:45:46 -07:00
|
|
|
Message::ProfileExpand(profile_id) => {
|
|
|
|
|
self.profile_expanded = Some(profile_id);
|
|
|
|
|
}
|
2025-11-06 12:33:54 +01:00
|
|
|
Message::ProfileHold(profile_id, drain_on_exit) => {
|
2024-04-14 21:17:32 +02:00
|
|
|
if let Some(profile) = self.config.profiles.get_mut(&profile_id) {
|
2025-11-06 12:33:54 +01:00
|
|
|
profile.drain_on_exit = drain_on_exit;
|
2024-04-14 21:17:32 +02:00
|
|
|
return self.save_profiles();
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-09 15:45:46 -07:00
|
|
|
Message::ProfileName(profile_id, text) => {
|
|
|
|
|
if let Some(profile) = self.config.profiles.get_mut(&profile_id) {
|
|
|
|
|
profile.name = text;
|
|
|
|
|
return self.save_profiles();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::ProfileNew => {
|
|
|
|
|
// Get next profile ID
|
|
|
|
|
let profile_id = self
|
|
|
|
|
.config
|
|
|
|
|
.profiles
|
|
|
|
|
.last_key_value()
|
|
|
|
|
.map(|(id, _)| ProfileId(id.0 + 1))
|
|
|
|
|
.unwrap_or_default();
|
|
|
|
|
self.config.profiles.insert(profile_id, Profile::default());
|
|
|
|
|
self.profile_expanded = Some(profile_id);
|
|
|
|
|
return self.save_profiles();
|
|
|
|
|
}
|
|
|
|
|
Message::ProfileOpen(profile_id) => {
|
2026-03-06 13:53:46 +01:00
|
|
|
return self.create_and_focus_new_terminal(
|
|
|
|
|
self.pane_model.focused(),
|
|
|
|
|
Some(profile_id),
|
|
|
|
|
false,
|
|
|
|
|
);
|
2024-02-09 15:45:46 -07:00
|
|
|
}
|
|
|
|
|
Message::ProfileRemove(profile_id) => {
|
|
|
|
|
// Reset matching terminals to default profile
|
|
|
|
|
for (_pane, 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();
|
|
|
|
|
if terminal.profile_id_opt == Some(profile_id) {
|
|
|
|
|
terminal.profile_id_opt = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-26 04:47:52 +01:00
|
|
|
if Some(profile_id) == self.get_default_profile() {
|
|
|
|
|
config_set!(default_profile, None);
|
|
|
|
|
}
|
2024-02-09 15:45:46 -07:00
|
|
|
self.config.profiles.remove(&profile_id);
|
|
|
|
|
return self.save_profiles();
|
|
|
|
|
}
|
2024-02-22 11:18:13 -07:00
|
|
|
Message::ProfileSyntaxTheme(profile_id, color_scheme_kind, theme_i) => {
|
|
|
|
|
match self
|
|
|
|
|
.theme_names(color_scheme_kind)
|
|
|
|
|
.get(theme_i)
|
|
|
|
|
.map(|x| x.to_string())
|
|
|
|
|
{
|
2024-02-09 15:45:46 -07:00
|
|
|
Some(theme_name) => {
|
|
|
|
|
if let Some(profile) = self.config.profiles.get_mut(&profile_id) {
|
2024-02-22 11:18:13 -07:00
|
|
|
match color_scheme_kind {
|
|
|
|
|
ColorSchemeKind::Dark => {
|
|
|
|
|
profile.syntax_theme_dark = theme_name;
|
|
|
|
|
}
|
|
|
|
|
ColorSchemeKind::Light => {
|
|
|
|
|
profile.syntax_theme_light = theme_name;
|
|
|
|
|
}
|
2024-02-09 15:45:46 -07:00
|
|
|
}
|
|
|
|
|
return self.save_profiles();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
log::warn!("failed to find syntax theme with index {}", theme_i);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::ProfileTabTitle(profile_id, text) => {
|
|
|
|
|
if let Some(profile) = self.config.profiles.get_mut(&profile_id) {
|
|
|
|
|
profile.tab_title = text;
|
|
|
|
|
return self.save_profiles();
|
|
|
|
|
}
|
|
|
|
|
}
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
Message::SaveOutput(entity_opt) => {
|
|
|
|
|
if self.dialog_opt.is_none() {
|
|
|
|
|
let pane = self.pane_model.focused();
|
|
|
|
|
if let Some(tab_model) = self.pane_model.panes.get(pane) {
|
|
|
|
|
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
|
|
|
|
|
let filename = format!(
|
|
|
|
|
"{}.txt",
|
|
|
|
|
tab_model
|
|
|
|
|
.text(entity)
|
|
|
|
|
.map(|title| title.replace(['/', '\0'], "-"))
|
|
|
|
|
.unwrap_or_else(|| fl!("new-terminal"))
|
|
|
|
|
);
|
|
|
|
|
let (dialog, command) = Dialog::new(
|
|
|
|
|
DialogSettings::new().kind(DialogKind::SaveFile { filename }),
|
|
|
|
|
|msg| Message::DialogMessage(Box::new(msg)),
|
|
|
|
|
move |result| Message::SaveOutputResult(pane, entity, result),
|
|
|
|
|
);
|
|
|
|
|
self.dialog_opt = Some(dialog);
|
|
|
|
|
return command;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::SaveOutputResult(pane, entity, result) => {
|
|
|
|
|
self.dialog_opt = None;
|
|
|
|
|
if let DialogResult::Open(paths) = result {
|
|
|
|
|
let path = paths[0].clone();
|
|
|
|
|
if let Some(tab_model) = self.pane_model.panes.get(pane)
|
|
|
|
|
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
{
|
|
|
|
|
let text = terminal.lock().unwrap().scrollback_text();
|
|
|
|
|
// Write in a blocking task to avoid stalling the UI on
|
|
|
|
|
// large scrollbacks or slow storage
|
|
|
|
|
return Task::perform(
|
|
|
|
|
async move {
|
|
|
|
|
match tokio::task::spawn_blocking(move || fs::write(&path, text))
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(Ok(())) => {}
|
|
|
|
|
Ok(Err(err)) => {
|
|
|
|
|
log::error!("failed to save terminal output: {}", err);
|
|
|
|
|
}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::error!("failed to save terminal output: {}", err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|()| action::none(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-22 15:00:50 -07:00
|
|
|
Message::SelectAll(entity_opt) => {
|
2024-01-17 16:09:56 +01:00
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.select_all();
|
|
|
|
|
}
|
2023-12-22 15:00:50 -07:00
|
|
|
}
|
2024-02-05 19:35:03 +01:00
|
|
|
return self.update_focus();
|
2023-12-22 15:00:50 -07:00
|
|
|
}
|
2023-12-22 15:40:10 -07:00
|
|
|
Message::ShowHeaderBar(show_headerbar) => {
|
|
|
|
|
if show_headerbar != self.config.show_headerbar {
|
2024-08-20 07:26:46 -06:00
|
|
|
config_set!(show_headerbar, show_headerbar);
|
|
|
|
|
return self.update_config();
|
2023-12-22 15:40:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-10-01 14:38:17 +02:00
|
|
|
Message::CopyOnSelectClipboard(copy_on_select_clipboard) => {
|
|
|
|
|
if copy_on_select_clipboard != self.config.copy_on_select_clipboard {
|
|
|
|
|
config_set!(copy_on_select_clipboard, copy_on_select_clipboard);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::SelectionDone(pane, entity) => {
|
|
|
|
|
// [yoda] Copie à la sélection vers le presse-papiers ; la
|
|
|
|
|
// sélection primaire, elle, suit le relâchement du bouton.
|
|
|
|
|
if self.config.copy_on_select_clipboard
|
|
|
|
|
&& let Some(tab_model) = self.pane_model.panes.get(pane)
|
|
|
|
|
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
{
|
|
|
|
|
let text = terminal.lock().unwrap().term.lock().selection_to_string();
|
|
|
|
|
if let Some(text) = text.filter(|text| !text.is_empty()) {
|
|
|
|
|
return clipboard::write(text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-03-06 13:53:46 +01:00
|
|
|
Message::TabNewInheritWorkingDirectory(tab_new_inherit_working_directory) => {
|
|
|
|
|
config_set!(
|
|
|
|
|
tab_new_inherit_working_directory,
|
|
|
|
|
tab_new_inherit_working_directory
|
|
|
|
|
);
|
|
|
|
|
}
|
2026-04-06 18:38:10 -06:00
|
|
|
Message::ShowPaneBorders(show_pane_borders) => {
|
|
|
|
|
if show_pane_borders != self.config.show_pane_borders {
|
|
|
|
|
config_set!(show_pane_borders, show_pane_borders);
|
|
|
|
|
}
|
|
|
|
|
}
|
yoda: attenuation des panneaux non focalises, a la Ghostty
Avec Claude Code et Modjo cote a cote, rien ne distinguait le panneau ou
l'on tape, hors le curseur creux. L'amont assombrissait le fond de 8 %
(aussi quand la fenetre perdait le focus) et l'a retire en juillet
(a43b5ae).
Reprise de unfocused-split-opacity de Ghostty : un voile de la couleur du
fond, texte compris, sur chaque panneau non focalise, seulement quand il y
en a plusieurs ; la perte de focus de la fenetre ne change rien.
- Config unfocused_pane_dim (pourcentage, defaut 30 = opacite 0,7 de
Ghostty, 0 desactive, plafond 85 = opacite minimale 0,15 de Ghostty).
- Reglages > Panneaux : curseur « Attenuation des panneaux inactifs ».
- terminal_box : voile trace dans un calque a part en fin de rendu (dans
le meme calque, la primitive de texte passerait par-dessus), aux coins
du fond du panneau.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:40:47 +02:00
|
|
|
Message::UnfocusedPaneDim(dim) => {
|
|
|
|
|
config_set!(
|
|
|
|
|
unfocused_pane_dim,
|
|
|
|
|
cmp::min(config::UNFOCUSED_PANE_DIM_MAX, dim)
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-01-11 17:47:24 +03:00
|
|
|
Message::UseBrightBold(use_bright_bold) => {
|
|
|
|
|
if use_bright_bold != self.config.use_bright_bold {
|
2024-08-20 07:26:46 -06:00
|
|
|
config_set!(use_bright_bold, use_bright_bold);
|
|
|
|
|
return self.update_config();
|
2024-01-11 17:47:24 +03:00
|
|
|
}
|
|
|
|
|
}
|
2026-07-09 14:18:02 +02:00
|
|
|
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)]);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-02-28 09:15:58 -07:00
|
|
|
Message::ShowAdvancedFontSettings(show) => {
|
|
|
|
|
self.show_advanced_font_settings = show;
|
2024-01-22 16:35:23 +01:00
|
|
|
}
|
2024-01-31 15:18:20 -07:00
|
|
|
Message::SystemThemeChange => {
|
2023-12-22 14:31:01 -07:00
|
|
|
return self.update_config();
|
|
|
|
|
}
|
2024-02-22 11:18:13 -07:00
|
|
|
Message::SyntaxTheme(color_scheme_kind, index) => {
|
|
|
|
|
match self.theme_names(color_scheme_kind).get(index) {
|
|
|
|
|
Some(theme_name) => {
|
|
|
|
|
match color_scheme_kind {
|
|
|
|
|
ColorSchemeKind::Dark => {
|
2024-08-20 07:26:46 -06:00
|
|
|
config_set!(syntax_theme_dark, theme_name.to_string());
|
2024-02-22 11:18:13 -07:00
|
|
|
}
|
|
|
|
|
ColorSchemeKind::Light => {
|
2024-08-20 07:26:46 -06:00
|
|
|
config_set!(syntax_theme_light, theme_name.to_string());
|
2024-02-22 11:18:13 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-08-20 07:26:46 -06:00
|
|
|
return self.update_config();
|
2024-02-22 11:18:13 -07:00
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
log::warn!("failed to find syntax theme with index {}", index);
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-22 11:18:13 -07:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
Message::TabActivate(entity) => {
|
2024-01-17 16:09:56 +01:00
|
|
|
if let Some(tab_model) = self.pane_model.active_mut() {
|
|
|
|
|
tab_model.activate(entity);
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
|
|
|
|
|
// Viewing the tab acknowledges any monitor alert
|
|
|
|
|
let mut icon_update = None;
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
if terminal.monitor_alert.is_some() {
|
|
|
|
|
terminal.monitor_alert = None;
|
|
|
|
|
icon_update = Some(monitor_icon_name(&terminal));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(icon_name) = icon_update {
|
|
|
|
|
monitor_update_tab_icon(tab_model, entity, icon_name);
|
|
|
|
|
}
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
self.sync_terminal_focus();
|
2024-01-17 16:09:56 +01:00
|
|
|
return self.update_title(None);
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
2024-01-18 02:44:25 -05:00
|
|
|
Message::TabActivateJump(pos) => {
|
2024-01-18 20:43:47 -07:00
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
// Length is always at least one so there shouldn't be a division by zero
|
|
|
|
|
let len = tab_model.iter().count();
|
|
|
|
|
// The typical pattern is that 1-8 selects tabs 1-8 while 9 selects the last tab
|
|
|
|
|
let pos = if pos >= 8 || pos > len - 1 {
|
|
|
|
|
len - 1
|
|
|
|
|
} else {
|
|
|
|
|
pos % len
|
|
|
|
|
};
|
2024-01-18 02:44:25 -05:00
|
|
|
|
2024-01-18 20:43:47 -07:00
|
|
|
let entity = tab_model.iter().nth(pos);
|
|
|
|
|
if let Some(entity) = entity {
|
|
|
|
|
return self.update(Message::TabActivate(entity));
|
|
|
|
|
}
|
2024-01-18 02:44:25 -05:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-09 10:16:32 -07:00
|
|
|
Message::TabClose(entity_opt) => {
|
2024-01-17 16:09:56 +01:00
|
|
|
if let Some(tab_model) = self.pane_model.active_mut() {
|
|
|
|
|
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
|
|
|
|
|
|
2026-02-12 20:36:47 -05:00
|
|
|
// Activate closest item if closing active tab
|
|
|
|
|
if entity == tab_model.active()
|
|
|
|
|
&& let Some(position) = tab_model.position(entity)
|
|
|
|
|
{
|
2024-01-17 16:09:56 +01:00
|
|
|
if position > 0 {
|
|
|
|
|
tab_model.activate_position(position - 1);
|
|
|
|
|
} else {
|
|
|
|
|
tab_model.activate_position(position + 1);
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
2023-12-17 22:51:50 -07:00
|
|
|
|
2024-01-17 16:09:56 +01:00
|
|
|
// Remove item
|
|
|
|
|
tab_model.remove(entity);
|
2023-12-20 13:31:10 -07:00
|
|
|
|
2024-01-18 10:15:46 +01:00
|
|
|
// If that was the last tab, close current pane
|
2024-01-17 16:09:56 +01:00
|
|
|
if tab_model.iter().next().is_none() {
|
|
|
|
|
if let Some((_state, sibling)) =
|
2025-01-21 17:07:45 +01:00
|
|
|
self.pane_model.panes.close(self.pane_model.focused())
|
2024-01-17 16:09:56 +01:00
|
|
|
{
|
2025-01-21 17:07:45 +01:00
|
|
|
self.terminal_ids.remove(&self.pane_model.focused());
|
|
|
|
|
self.pane_model.set_focus(sibling);
|
2024-01-17 16:09:56 +01:00
|
|
|
} else {
|
2024-01-18 10:15:46 +01:00
|
|
|
//Last pane, closing window
|
2024-10-22 12:31:02 -06:00
|
|
|
if let Some(window_id) = self.core.main_window_id() {
|
|
|
|
|
return window::close(window_id);
|
|
|
|
|
}
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
self.sync_terminal_focus();
|
2024-01-17 16:09:56 +01:00
|
|
|
return self.update_title(None);
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
2023-12-22 15:00:50 -07:00
|
|
|
Message::TabContextAction(entity, action) => {
|
2026-03-18 11:13:08 -06:00
|
|
|
let mut tasks = Vec::new();
|
2026-04-08 12:31:30 -06:00
|
|
|
// Close terminal context menu state
|
2026-04-10 18:09:25 +02:00
|
|
|
if let Some(tab_model) = self.pane_model.active()
|
2026-04-10 18:32:09 +02:00
|
|
|
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
{
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
//Some actions need the menu_state,
|
|
|
|
|
//so only clear the position for them.
|
|
|
|
|
match action {
|
2026-09-08 13:49:17 -06:00
|
|
|
// these read the link from the menu state and clear it themselves
|
|
|
|
|
Action::LaunchUrlByMenu | Action::CopyUrlByMenu => {}
|
2026-04-10 18:32:09 +02:00
|
|
|
_ => {
|
|
|
|
|
terminal.context_menu = None;
|
|
|
|
|
}
|
2023-12-22 15:00:50 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-18 11:13:08 -06:00
|
|
|
tasks.push(self.update(action.message(Some(entity))));
|
|
|
|
|
return cosmic::Task::batch(tasks);
|
2023-12-22 15:00:50 -07:00
|
|
|
}
|
2025-10-16 09:27:00 +02:00
|
|
|
Message::TabContextMenu(pane, menu_state) => {
|
2026-03-18 11:13:08 -06:00
|
|
|
// 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();
|
2026-09-08 13:49:17 -06:00
|
|
|
if terminal.context_menu.take().is_some() {
|
|
|
|
|
terminal.active_regex_match = None;
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
}
|
2026-03-18 11:13:08 -06:00
|
|
|
}
|
2023-12-22 15:00:50 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-02 13:56:17 +01:00
|
|
|
|
2026-09-08 13:49:17 -06:00
|
|
|
// 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);
|
2023-12-22 15:00:50 -07:00
|
|
|
}
|
2026-03-18 11:13:08 -06:00
|
|
|
self.pane_model.set_focus(pane);
|
2023-12-22 15:00:50 -07:00
|
|
|
}
|
|
|
|
|
}
|
2024-02-09 15:45:46 -07:00
|
|
|
Message::TabNew => {
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
// The profile from `--profile` only applies to the first tab
|
|
|
|
|
let profile_id_opt = self
|
|
|
|
|
.startup_profile_id
|
|
|
|
|
.take()
|
|
|
|
|
.or_else(|| self.get_default_profile());
|
2024-02-26 04:47:52 +01:00
|
|
|
return self.create_and_focus_new_terminal(
|
2025-01-21 17:07:45 +01:00
|
|
|
self.pane_model.focused(),
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
profile_id_opt,
|
2026-03-06 13:53:46 +01:00
|
|
|
self.config.tab_new_inherit_working_directory,
|
2025-09-10 14:33:46 +02:00
|
|
|
);
|
2024-02-09 15:45:46 -07:00
|
|
|
}
|
2024-08-13 02:20:52 -04:00
|
|
|
Message::TabNewNoProfile => {
|
2026-03-06 13:53:46 +01:00
|
|
|
return self.create_and_focus_new_terminal(
|
|
|
|
|
self.pane_model.focused(),
|
|
|
|
|
None,
|
|
|
|
|
self.config.tab_new_inherit_working_directory,
|
|
|
|
|
);
|
2024-08-13 02:20:52 -04:00
|
|
|
}
|
2024-01-17 22:58:04 -05:00
|
|
|
Message::TabNext => {
|
2024-01-18 20:43:47 -07:00
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let len = tab_model.iter().count();
|
|
|
|
|
// Next tab position. Wraps around to 0 (first tab) if the last tab is active.
|
|
|
|
|
let pos = tab_model
|
|
|
|
|
.position(tab_model.active())
|
|
|
|
|
.map(|i| (i as usize + 1) % len)
|
|
|
|
|
.expect("at least one tab is always open");
|
|
|
|
|
|
|
|
|
|
let entity = tab_model.iter().nth(pos);
|
|
|
|
|
if let Some(entity) = entity {
|
|
|
|
|
return self.update(Message::TabActivate(entity));
|
|
|
|
|
}
|
2024-01-17 22:58:04 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::TabPrev => {
|
2024-01-18 20:43:47 -07:00
|
|
|
if let Some(tab_model) = self.pane_model.active() {
|
|
|
|
|
let pos = tab_model
|
|
|
|
|
.position(tab_model.active())
|
|
|
|
|
.and_then(|i| (i as usize).checked_sub(1))
|
2026-04-10 18:32:09 +02:00
|
|
|
.unwrap_or_else(|| tab_model.iter().count().saturating_sub(1));
|
2024-01-18 20:43:47 -07:00
|
|
|
|
|
|
|
|
let entity = tab_model.iter().nth(pos);
|
|
|
|
|
if let Some(entity) = entity {
|
|
|
|
|
return self.update(Message::TabActivate(entity));
|
|
|
|
|
}
|
2024-01-17 22:58:04 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-05-24 10:27:32 +02:00
|
|
|
Message::TabRenameOpenForPane(pane, entity) => {
|
|
|
|
|
self.pane_model.set_focus(pane);
|
|
|
|
|
return self.update(Message::TabRenameOpen(Some(entity)));
|
|
|
|
|
}
|
|
|
|
|
Message::TabRenameOpen(entity_opt) => {
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
let pane = self.pane_model.focused();
|
|
|
|
|
if let Some(tab_model) = self.pane_model.panes.get(pane) {
|
2026-05-24 10:27:32 +02:00
|
|
|
let entity = entity_opt.unwrap_or_else(|| tab_model.active());
|
|
|
|
|
let initial = tab_model
|
|
|
|
|
.text(entity)
|
|
|
|
|
.map(|s| s.to_string())
|
|
|
|
|
.unwrap_or_default();
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
self.rename_tab = Some((pane, entity, initial));
|
2026-05-24 10:27:32 +02:00
|
|
|
self.pane_model.unfocus_all_terminals();
|
|
|
|
|
return self.update_focus();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::TabRenameInput(value) => {
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
if let Some((_, _, buffer)) = self.rename_tab.as_mut() {
|
2026-05-24 10:27:32 +02:00
|
|
|
*buffer = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::TabRenameCommit => {
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
if let Some((pane, entity, value)) = self.rename_tab.take() {
|
2026-05-24 10:27:32 +02:00
|
|
|
let trimmed = value.trim().to_string();
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
let mut renamed = false;
|
|
|
|
|
if let Some(tab_model) = self.pane_model.panes.get_mut(pane) {
|
2026-05-24 10:27:32 +02:00
|
|
|
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);
|
|
|
|
|
}
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
renamed = true;
|
|
|
|
|
}
|
|
|
|
|
if renamed {
|
|
|
|
|
self.pane_model.set_focus(pane);
|
2026-05-24 10:27:32 +02:00
|
|
|
}
|
|
|
|
|
return self.update_title(None);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::TabRenameCancel => {
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
if let Some((pane, _, _)) = self.rename_tab.take()
|
|
|
|
|
&& self.pane_model.panes.get(pane).is_some()
|
|
|
|
|
{
|
|
|
|
|
self.pane_model.set_focus(pane);
|
|
|
|
|
return self.update_title(None);
|
|
|
|
|
}
|
2026-05-24 10:27:32 +02:00
|
|
|
self.pane_model.update_terminal_focus();
|
|
|
|
|
}
|
|
|
|
|
Message::TabRenameResetToDefault => {
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
if let Some((pane, entity, _)) = self.rename_tab.take() {
|
|
|
|
|
let mut reset = false;
|
|
|
|
|
if let Some(tab_model) = self.pane_model.panes.get_mut(pane) {
|
|
|
|
|
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"));
|
|
|
|
|
reset = true;
|
|
|
|
|
}
|
|
|
|
|
if reset {
|
|
|
|
|
self.pane_model.set_focus(pane);
|
|
|
|
|
return self.update_title(None);
|
2026-05-24 10:27:32 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
self.pane_model.update_terminal_focus();
|
|
|
|
|
}
|
2024-01-17 16:09:56 +01:00
|
|
|
Message::TermEvent(pane, entity, event) => {
|
|
|
|
|
match event {
|
|
|
|
|
TermEvent::Bell => {
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
// Comme le défaut de Ghostty (`bell-features = attention,title`),
|
|
|
|
|
// sans la demande d'attention xdg-activation : cosmic-comp donne
|
|
|
|
|
// un jeton valide à toute application non sandboxée, la fenêtre
|
|
|
|
|
// volerait le focus. Il reste l'indicateur d'onglet.
|
|
|
|
|
if !self.tab_in_view(pane, entity) {
|
|
|
|
|
self.set_tab_attention(pane, entity, AttentionKind::Bell);
|
|
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
2024-03-13 09:12:07 -06:00
|
|
|
TermEvent::ClipboardLoad(kind, callback) => {
|
|
|
|
|
match kind {
|
|
|
|
|
term::ClipboardType::Clipboard => {
|
|
|
|
|
log::info!("clipboard load");
|
2024-10-22 12:31:02 -06:00
|
|
|
return clipboard::read().map(move |data_opt| {
|
2024-03-13 09:12:07 -06:00
|
|
|
//TODO: what to do when data_opt is None?
|
|
|
|
|
callback(&data_opt.unwrap_or_default());
|
|
|
|
|
// We don't need to do anything else
|
2025-03-19 12:27:00 -04:00
|
|
|
action::none()
|
2024-03-13 09:12:07 -06:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
term::ClipboardType::Selection => {
|
|
|
|
|
log::info!("TODO: load selection");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
TermEvent::ClipboardStore(kind, data) => match kind {
|
|
|
|
|
term::ClipboardType::Clipboard => {
|
|
|
|
|
log::info!("clipboard store");
|
|
|
|
|
return clipboard::write(data);
|
|
|
|
|
}
|
|
|
|
|
term::ClipboardType::Selection => {
|
|
|
|
|
log::info!("TODO: store selection");
|
|
|
|
|
}
|
|
|
|
|
},
|
2024-01-17 16:09:56 +01:00
|
|
|
TermEvent::ColorRequest(index, f) => {
|
2026-04-10 18:09:25 +02:00
|
|
|
if let Some(tab_model) = self.pane_model.panes.get(pane)
|
2026-04-10 18:32:09 +02:00
|
|
|
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
{
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
2026-06-19 18:52:18 +02:00
|
|
|
let rgb = terminal.effective_color(index);
|
2026-04-10 18:32:09 +02:00
|
|
|
let text = f(rgb);
|
yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)
Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.
- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
plus la vue en bas. Couvre touches legacy et kitty, collage, clic
milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
application attendrait une reponse qui ne vient pas (CSI ? u de Claude
Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
(selection, copie et defilement locaux, meme dans une application qui
la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
action ToggleReadOnly dans les raccourcis (groupe Autre, sans
raccourci par defaut, comme Ghostty et Konsole) ; cadenas
changes-prevent-symbolic sur l'onglet, apres les alertes.
Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:19:02 +02:00
|
|
|
terminal.respond(text.into_bytes());
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
2024-03-13 09:12:07 -06:00
|
|
|
TermEvent::CursorBlinkingChange => {
|
|
|
|
|
//TODO: should we blink the cursor?
|
|
|
|
|
}
|
2024-01-17 16:09:56 +01:00
|
|
|
TermEvent::Exit => {
|
|
|
|
|
return self.update(Message::TabClose(Some(entity)));
|
2023-12-17 22:51:50 -07:00
|
|
|
}
|
2024-01-17 16:09:56 +01:00
|
|
|
TermEvent::PtyWrite(text) => {
|
2026-04-10 18:09:25 +02:00
|
|
|
if let Some(tab_model) = self.pane_model.panes.get(pane)
|
2026-04-10 18:32:09 +02:00
|
|
|
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
{
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)
Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.
- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
plus la vue en bas. Couvre touches legacy et kitty, collage, clic
milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
application attendrait une reponse qui ne vient pas (CSI ? u de Claude
Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
(selection, copie et defilement locaux, meme dans une application qui
la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
action ToggleReadOnly dans les raccourcis (groupe Autre, sans
raccourci par defaut, comme Ghostty et Konsole) ; cadenas
changes-prevent-symbolic sur l'onglet, apres les alertes.
Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:19:02 +02:00
|
|
|
terminal.respond(text.into_bytes());
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
TermEvent::ResetTitle => {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.panes.get_mut(pane) {
|
2024-02-20 10:21:30 -07:00
|
|
|
let tab_title_override =
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.tab_title_override.clone()
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
tab_model.text_set(
|
|
|
|
|
entity,
|
|
|
|
|
tab_title_override.unwrap_or_else(|| fl!("new-terminal")),
|
|
|
|
|
);
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
return self.update_title(None);
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
|
|
|
|
TermEvent::TextAreaSizeRequest(f) => {
|
2026-04-10 18:09:25 +02:00
|
|
|
if let Some(tab_model) = self.pane_model.panes.get(pane)
|
2026-04-10 18:32:09 +02:00
|
|
|
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
{
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
let text = f(terminal.size().into());
|
yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)
Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.
- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
plus la vue en bas. Couvre touches legacy et kitty, collage, clic
milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
application attendrait une reponse qui ne vient pas (CSI ? u de Claude
Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
(selection, copie et defilement locaux, meme dans une application qui
la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
action ToggleReadOnly dans les raccourcis (groupe Autre, sans
raccourci par defaut, comme Ghostty et Konsole) ; cadenas
changes-prevent-symbolic sur l'onglet, apres les alertes.
Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:19:02 +02:00
|
|
|
terminal.respond(text.into_bytes());
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
TermEvent::Title(title) => {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.panes.get_mut(pane) {
|
2024-02-20 10:21:30 -07:00
|
|
|
let has_override =
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.tab_title_override.is_some()
|
|
|
|
|
} else {
|
|
|
|
|
false
|
|
|
|
|
};
|
|
|
|
|
if !has_override {
|
|
|
|
|
tab_model.text_set(entity, title);
|
|
|
|
|
}
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
return self.update_title(None);
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
TermEvent::MouseCursorDirty => {
|
2026-04-10 18:09:25 +02:00
|
|
|
if let Some(tab_model) = self.pane_model.panes.get(pane)
|
2026-04-10 18:32:09 +02:00
|
|
|
&& let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
{
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.needs_update = true;
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
|
|
|
|
}
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
TermEvent::Wakeup => {
|
|
|
|
|
if let Some(tab_model) = self.pane_model.panes.get_mut(pane) {
|
|
|
|
|
let is_active_tab = tab_model.active() == entity;
|
|
|
|
|
let mut icon_update = None;
|
|
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.needs_update = true;
|
|
|
|
|
if terminal.monitoring_enabled() {
|
|
|
|
|
terminal.monitor_last_output = std::time::Instant::now();
|
|
|
|
|
if terminal.monitor_alert == Some(MonitorKind::Silence) {
|
|
|
|
|
terminal.monitor_alert = None;
|
|
|
|
|
icon_update = Some(monitor_icon_name(&terminal));
|
|
|
|
|
}
|
|
|
|
|
if terminal.monitor_activity
|
|
|
|
|
&& !is_active_tab
|
|
|
|
|
&& terminal.monitor_alert.is_none()
|
|
|
|
|
{
|
|
|
|
|
terminal.monitor_alert = Some(MonitorKind::Activity);
|
|
|
|
|
icon_update = Some(monitor_icon_name(&terminal));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let Some(icon_name) = icon_update {
|
|
|
|
|
monitor_update_tab_icon(tab_model, entity, icon_name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-05 16:35:17 +02:00
|
|
|
TermEvent::ChildExit(_error_code) => {
|
|
|
|
|
//Ignore this for now
|
2024-04-08 08:44:32 -06:00
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
Message::TermEventTx(term_event_tx) => {
|
2024-02-02 08:44:28 -07:00
|
|
|
// Check if the terminal event channel was reset
|
|
|
|
|
if self.term_event_tx_opt.is_some() {
|
|
|
|
|
// Close tabs using old terminal event channel
|
|
|
|
|
log::warn!("terminal event channel reset, closing tabs");
|
|
|
|
|
|
|
|
|
|
// First, close other panes
|
|
|
|
|
while let Some((_state, sibling)) =
|
2025-01-21 17:07:45 +01:00
|
|
|
self.pane_model.panes.close(self.pane_model.focused())
|
2024-02-02 08:44:28 -07:00
|
|
|
{
|
2025-01-21 17:07:45 +01:00
|
|
|
self.terminal_ids.remove(&self.pane_model.focused());
|
|
|
|
|
self.pane_model.set_focus(sibling);
|
2024-02-02 08:44:28 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Next, close all tabs in the active pane
|
|
|
|
|
if let Some(tab_model) = self.pane_model.active_mut() {
|
|
|
|
|
let entities: Vec<_> = tab_model.iter().collect();
|
|
|
|
|
for entity in entities {
|
|
|
|
|
tab_model.remove(entity);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Set new terminal event channel
|
2023-12-20 13:31:10 -07:00
|
|
|
self.term_event_tx_opt = Some(term_event_tx);
|
2024-02-02 08:44:28 -07:00
|
|
|
|
|
|
|
|
// Spawn first tab
|
|
|
|
|
return self.update(Message::TabNew);
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
Message::TermOsc(pane, entity, event) => match event {
|
|
|
|
|
OscEvent::Notify { title, body } => {
|
|
|
|
|
// Rien à signaler pour l'onglet qu'on a sous les yeux
|
|
|
|
|
if self.tab_in_view(pane, entity) {
|
|
|
|
|
return Task::none();
|
|
|
|
|
}
|
|
|
|
|
self.set_tab_attention(pane, entity, AttentionKind::Notification);
|
|
|
|
|
|
|
|
|
|
let Some(tab_model) = self.pane_model.panes.get(pane) else {
|
|
|
|
|
return Task::none();
|
|
|
|
|
};
|
|
|
|
|
let allowed = tab_model
|
|
|
|
|
.data::<Mutex<Terminal>>(entity)
|
|
|
|
|
.is_some_and(|terminal| {
|
|
|
|
|
terminal
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap()
|
|
|
|
|
.notify_allowed(title.as_deref(), &body)
|
|
|
|
|
});
|
|
|
|
|
if !allowed {
|
|
|
|
|
return Task::none();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Titre de la séquence, suivi de l'onglet d'où elle vient :
|
|
|
|
|
// plusieurs sessions Claude Code se distinguent ainsi.
|
|
|
|
|
let tab_title = tab_model.text(entity).unwrap_or_default().to_string();
|
|
|
|
|
let summary = match title {
|
|
|
|
|
Some(title) if !tab_title.is_empty() && title != tab_title => {
|
|
|
|
|
format!("{title} · {tab_title}")
|
|
|
|
|
}
|
|
|
|
|
Some(title) => title,
|
|
|
|
|
None if !tab_title.is_empty() => tab_title,
|
|
|
|
|
None => fl!("cosmic-terminal"),
|
|
|
|
|
};
|
|
|
|
|
if let Some(notify_tx) = &self.notify_tx_opt {
|
|
|
|
|
let _ = notify_tx.send(desktop_notify::Request::Show {
|
|
|
|
|
tab: (pane, entity),
|
|
|
|
|
summary,
|
|
|
|
|
body,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Message::DesktopNotify(event) => match event {
|
|
|
|
|
desktop_notify::Event::Ready(notify_tx) => {
|
|
|
|
|
self.notify_tx_opt = Some(notify_tx);
|
|
|
|
|
}
|
|
|
|
|
desktop_notify::Event::Activated {
|
|
|
|
|
tab: (pane, entity),
|
|
|
|
|
token,
|
|
|
|
|
} => {
|
|
|
|
|
// Clic sur la notification : retour à l'onglet d'origine,
|
|
|
|
|
// puis la fenêtre au premier plan avec le jeton
|
|
|
|
|
// xdg-activation fourni par le serveur de notifications.
|
|
|
|
|
let mut tasks = Vec::new();
|
|
|
|
|
if let Some(tab_model) = self.pane_model.panes.get_mut(pane)
|
|
|
|
|
&& tab_model.contains_item(entity)
|
|
|
|
|
{
|
|
|
|
|
tab_model.activate(entity);
|
|
|
|
|
self.pane_model.set_focus(pane);
|
|
|
|
|
tasks.push(self.update(Message::TabActivate(entity)));
|
|
|
|
|
}
|
|
|
|
|
if let Some(token) = token {
|
|
|
|
|
tasks.push(Task::done(cosmic::Action::Cosmic(
|
|
|
|
|
cosmic::app::Action::Activate(token),
|
|
|
|
|
)));
|
|
|
|
|
}
|
|
|
|
|
return Task::batch(tasks);
|
|
|
|
|
}
|
|
|
|
|
},
|
2023-12-22 14:31:01 -07:00
|
|
|
Message::ToggleContextPage(context_page) => {
|
|
|
|
|
if self.context_page == context_page {
|
|
|
|
|
self.core.window.show_context = !self.core.window.show_context;
|
2025-10-24 17:56:10 -04:00
|
|
|
self.pane_model.update_terminal_focus();
|
|
|
|
|
|
2026-02-05 11:25:53 -07:00
|
|
|
if let ContextPage::KeyboardShortcuts = context_page {
|
|
|
|
|
self.shortcut_page_toggle();
|
|
|
|
|
}
|
|
|
|
|
|
2025-12-08 21:32:36 +01:00
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
if ContextPage::PasswordManager == context_page {
|
|
|
|
|
if self.core.window.show_context {
|
|
|
|
|
self.password_mgr.pane = Some(self.pane_model.focused());
|
|
|
|
|
return self.password_mgr.refresh_password_list();
|
|
|
|
|
} else {
|
|
|
|
|
self.password_mgr.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-10-24 17:56:10 -04:00
|
|
|
return self.update_focus();
|
2023-12-22 14:31:01 -07:00
|
|
|
} else {
|
|
|
|
|
self.context_page = context_page;
|
|
|
|
|
self.core.window.show_context = true;
|
2025-10-24 17:56:10 -04:00
|
|
|
self.pane_model.unfocus_all_terminals();
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
2024-02-22 09:55:46 -07:00
|
|
|
|
|
|
|
|
// Extra work to do to prepare context pages
|
2024-04-14 20:58:19 +01:00
|
|
|
if let ContextPage::ColorSchemes(color_scheme_kind) = self.context_page {
|
|
|
|
|
self.color_scheme_errors.clear();
|
|
|
|
|
self.color_scheme_expanded = None;
|
|
|
|
|
self.color_scheme_renaming = None;
|
|
|
|
|
self.color_scheme_tab_model = widget::segmented_button::Model::default();
|
|
|
|
|
let dark_entity = self
|
|
|
|
|
.color_scheme_tab_model
|
|
|
|
|
.insert()
|
|
|
|
|
.text(fl!("dark"))
|
|
|
|
|
.data(ColorSchemeKind::Dark)
|
|
|
|
|
.id();
|
|
|
|
|
let light_entity = self
|
|
|
|
|
.color_scheme_tab_model
|
|
|
|
|
.insert()
|
|
|
|
|
.text(fl!("light"))
|
|
|
|
|
.data(ColorSchemeKind::Light)
|
|
|
|
|
.id();
|
|
|
|
|
self.color_scheme_tab_model
|
|
|
|
|
.activate(match color_scheme_kind {
|
|
|
|
|
ColorSchemeKind::Dark => dark_entity,
|
|
|
|
|
ColorSchemeKind::Light => light_entity,
|
|
|
|
|
});
|
2024-02-22 09:55:46 -07:00
|
|
|
}
|
2024-02-09 09:35:24 +01:00
|
|
|
|
2026-02-05 10:34:58 -07:00
|
|
|
if let ContextPage::KeyboardShortcuts = context_page {
|
2026-02-05 11:25:53 -07:00
|
|
|
self.shortcut_page_toggle();
|
|
|
|
|
return self.update_focus();
|
2026-02-05 10:34:58 -07:00
|
|
|
}
|
|
|
|
|
|
2024-02-09 09:35:24 +01:00
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
if ContextPage::PasswordManager == context_page {
|
2025-12-08 21:32:36 +01:00
|
|
|
self.password_mgr.pane = Some(self.pane_model.focused());
|
|
|
|
|
return self.password_mgr.refresh_password_list();
|
2024-02-09 09:35:24 +01:00
|
|
|
}
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
2024-02-28 09:15:58 -07:00
|
|
|
Message::UpdateDefaultProfile((default, profile_id)) => {
|
|
|
|
|
config_set!(default_profile, default.then_some(profile_id));
|
2024-01-11 11:33:39 -07:00
|
|
|
}
|
2024-01-09 10:16:32 -07:00
|
|
|
Message::WindowClose => {
|
2024-10-22 12:31:02 -06:00
|
|
|
if let Some(window_id) = self.core.main_window_id() {
|
|
|
|
|
return window::close(window_id);
|
|
|
|
|
}
|
2024-01-09 10:16:32 -07:00
|
|
|
}
|
|
|
|
|
Message::WindowNew => match env::current_exe() {
|
2026-05-25 11:16:10 -06:00
|
|
|
Ok(exe) => {
|
|
|
|
|
let mut command = process::Command::new(&exe);
|
|
|
|
|
if self.config.tab_new_inherit_working_directory
|
|
|
|
|
&& let Some(dir) = self.active_terminal_working_directory()
|
|
|
|
|
{
|
|
|
|
|
command.arg("--working-directory");
|
|
|
|
|
command.arg(dir);
|
2024-01-09 10:16:32 -07:00
|
|
|
}
|
2026-05-25 11:16:10 -06:00
|
|
|
match command.spawn() {
|
|
|
|
|
Ok(_child) => {}
|
|
|
|
|
Err(err) => {
|
|
|
|
|
log::error!("failed to execute {:?}: {}", exe, err);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-09 10:16:32 -07:00
|
|
|
Err(err) => {
|
|
|
|
|
log::error!("failed to get current executable path: {}", err);
|
|
|
|
|
}
|
|
|
|
|
},
|
2025-01-21 17:07:45 +01:00
|
|
|
Message::WindowFocused => {
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
self.window_focused = true;
|
2025-10-24 17:56:10 -04:00
|
|
|
if !self.core.window.show_context {
|
|
|
|
|
self.pane_model.update_terminal_focus();
|
|
|
|
|
}
|
2025-01-21 17:07:45 +01:00
|
|
|
return self.update_focus();
|
|
|
|
|
}
|
|
|
|
|
Message::WindowUnfocused => {
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
self.window_focused = false;
|
2025-01-21 17:07:45 +01:00
|
|
|
self.pane_model.unfocus_all_terminals();
|
|
|
|
|
}
|
2024-01-09 10:16:32 -07:00
|
|
|
Message::ZoomIn => {
|
2024-08-20 04:52:34 +03:00
|
|
|
return self.update_render_active_pane_zoom(message);
|
2024-01-09 10:16:32 -07:00
|
|
|
}
|
|
|
|
|
Message::ZoomOut => {
|
2024-08-20 04:52:34 +03:00
|
|
|
return self.update_render_active_pane_zoom(message);
|
2024-01-09 10:16:32 -07:00
|
|
|
}
|
|
|
|
|
Message::ZoomReset => {
|
2026-04-27 20:33:44 +03:00
|
|
|
self.reset_active_pane_zoom();
|
2024-08-20 07:26:46 -06:00
|
|
|
return self.update_config();
|
2024-01-09 10:16:32 -07:00
|
|
|
}
|
2025-03-19 12:27:00 -04:00
|
|
|
Message::Surface(a) => {
|
2026-09-08 13:49:17 -06:00
|
|
|
return cosmic::task::message(cosmic::Action::Surface(a));
|
2025-03-19 12:27:00 -04:00
|
|
|
}
|
2026-01-28 20:08:44 -05:00
|
|
|
Message::ReorderTab(
|
|
|
|
|
pane,
|
|
|
|
|
ReorderEvent {
|
|
|
|
|
dragged,
|
|
|
|
|
target,
|
|
|
|
|
position,
|
|
|
|
|
},
|
|
|
|
|
) => {
|
|
|
|
|
let Some(p) = self.pane_model.panes.get_mut(pane) else {
|
|
|
|
|
log::error!("Failed to find reordered tab model.");
|
|
|
|
|
return Task::none();
|
|
|
|
|
};
|
|
|
|
|
_ = p.reorder(dragged, target, position);
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
|
2024-10-22 12:31:02 -06:00
|
|
|
Task::none()
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
|
2025-09-10 14:33:46 +02:00
|
|
|
fn context_drawer(&self) -> Option<context_drawer::ContextDrawer<'_, Message>> {
|
2023-12-22 14:31:01 -07:00
|
|
|
if !self.core.window.show_context {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Some(match self.context_page {
|
2025-09-11 16:25:29 +02:00
|
|
|
ContextPage::About => context_drawer::about(
|
|
|
|
|
&self.about,
|
2025-10-03 14:56:47 -04:00
|
|
|
|s| Message::LaunchUrl(s.to_string()),
|
2024-12-02 23:26:14 +01:00
|
|
|
Message::ToggleContextPage(ContextPage::About),
|
|
|
|
|
),
|
|
|
|
|
ContextPage::ColorSchemes(color_scheme_kind) => context_drawer::context_drawer(
|
|
|
|
|
self.color_schemes(color_scheme_kind),
|
|
|
|
|
Message::ToggleContextPage(ContextPage::ColorSchemes(color_scheme_kind)),
|
|
|
|
|
)
|
|
|
|
|
.title(fl!("color-schemes")),
|
2026-02-05 10:34:58 -07:00
|
|
|
ContextPage::KeyboardShortcuts => context_drawer::context_drawer(
|
|
|
|
|
self.keyboard_shortcuts(),
|
|
|
|
|
Message::ToggleContextPage(ContextPage::KeyboardShortcuts),
|
|
|
|
|
)
|
|
|
|
|
.title(fl!("keyboard-shortcuts")),
|
2024-12-02 23:26:14 +01:00
|
|
|
ContextPage::Profiles => context_drawer::context_drawer(
|
|
|
|
|
self.profiles(),
|
|
|
|
|
Message::ToggleContextPage(ContextPage::Profiles),
|
|
|
|
|
)
|
|
|
|
|
.title(fl!("profiles")),
|
|
|
|
|
ContextPage::Settings => context_drawer::context_drawer(
|
|
|
|
|
self.settings(),
|
|
|
|
|
Message::ToggleContextPage(ContextPage::Settings),
|
|
|
|
|
)
|
|
|
|
|
.title(fl!("settings")),
|
2024-02-09 09:35:24 +01:00
|
|
|
#[cfg(feature = "password_manager")]
|
|
|
|
|
ContextPage::PasswordManager => context_drawer::context_drawer(
|
|
|
|
|
self.password_mgr.context_page(self.core.system_theme()),
|
|
|
|
|
Message::ToggleContextPage(ContextPage::PasswordManager),
|
|
|
|
|
)
|
|
|
|
|
.title(fl!("passwords-title")),
|
2023-12-22 14:31:01 -07:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 22:08:34 -08:00
|
|
|
fn dialog(&self) -> Option<Element<'_, Message>> {
|
2026-07-09 14:18:02 +02:00
|
|
|
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(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
if let Some((_, _, buffer)) = self.rename_tab.as_ref() {
|
2026-05-24 10:27:32 +02:00
|
|
|
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(),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-03 22:08:34 -08:00
|
|
|
let conflict = self.shortcut_conflict.as_ref()?;
|
|
|
|
|
let binding = shortcuts::binding_display(&conflict.binding);
|
|
|
|
|
let existing = shortcuts::action_label(conflict.existing_action);
|
|
|
|
|
let new_action = shortcuts::action_label(conflict.new_action);
|
|
|
|
|
let body = fl!(
|
|
|
|
|
"shortcut-replace-body",
|
|
|
|
|
binding = binding.as_str(),
|
|
|
|
|
existing = existing.as_str(),
|
|
|
|
|
new_action = new_action.as_str()
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Some(
|
|
|
|
|
widget::dialog()
|
|
|
|
|
.title(fl!("shortcut-replace-title"))
|
|
|
|
|
.body(body)
|
|
|
|
|
.primary_action(
|
|
|
|
|
widget::button::suggested(fl!("replace"))
|
|
|
|
|
.on_press(Message::ShortcutConflictReplace),
|
|
|
|
|
)
|
|
|
|
|
.secondary_action(
|
|
|
|
|
widget::button::standard(fl!("cancel"))
|
|
|
|
|
.on_press(Message::ShortcutConflictCancel),
|
|
|
|
|
)
|
|
|
|
|
.into(),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-10 14:33:46 +02:00
|
|
|
fn header_start(&self) -> Vec<Element<'_, Self::Message>> {
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
vec![menu_bar(
|
|
|
|
|
&self.core,
|
|
|
|
|
&self.config,
|
|
|
|
|
&self.key_binds,
|
|
|
|
|
self.active_terminal_monitors(),
|
yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)
Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.
- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
plus la vue en bas. Couvre touches legacy et kitty, collage, clic
milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
application attendrait une reponse qui ne vient pas (CSI ? u de Claude
Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
(selection, copie et defilement locaux, meme dans une application qui
la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
action ToggleReadOnly dans les raccourcis (groupe Autre, sans
raccourci par defaut, comme Ghostty et Konsole) ; cadenas
changes-prevent-symbolic sur l'onglet, apres les alertes.
Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:19:02 +02:00
|
|
|
self.active_terminal_read_only(),
|
2026-07-09 14:18:02 +02:00
|
|
|
&self.ssh_hosts,
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
)]
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
|
|
|
|
|
2025-09-10 14:33:46 +02:00
|
|
|
fn header_end(&self) -> Vec<Element<'_, Self::Message>> {
|
2024-09-20 19:44:36 -06:00
|
|
|
vec![
|
|
|
|
|
widget::button::custom(icon_cache_get("list-add-symbolic", 16))
|
|
|
|
|
.on_press(Message::TabNew)
|
2024-09-24 00:41:12 +02:00
|
|
|
.padding(8)
|
2024-10-22 12:31:02 -06:00
|
|
|
.class(style::Button::Icon)
|
2024-09-20 19:44:36 -06:00
|
|
|
.into(),
|
|
|
|
|
]
|
2024-01-31 10:37:10 -07:00
|
|
|
}
|
|
|
|
|
|
2025-09-10 14:33:46 +02:00
|
|
|
fn view_window(&self, window_id: window::Id) -> Element<'_, Message> {
|
2024-02-22 09:55:46 -07:00
|
|
|
match &self.dialog_opt {
|
|
|
|
|
Some(dialog) => dialog.view(window_id),
|
|
|
|
|
None => widget::text("Unknown window ID").into(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
/// Creates a view after each update.
|
2025-09-10 14:33:46 +02:00
|
|
|
fn view(&self) -> Element<'_, Self::Message> {
|
2026-07-06 09:26:53 -04:00
|
|
|
let t = self.core().system_theme();
|
|
|
|
|
let cosmic = t.cosmic();
|
2026-04-06 18:38:10 -06:00
|
|
|
let cosmic_theme::Spacing {
|
|
|
|
|
space_xxxs,
|
|
|
|
|
space_xxs,
|
|
|
|
|
..
|
|
|
|
|
} = cosmic.spacing;
|
|
|
|
|
|
|
|
|
|
let show_pane_borders =
|
|
|
|
|
self.config.show_pane_borders && self.pane_model.panes.panes.len() > 1;
|
|
|
|
|
let pane_corner_radius: iced::border::Radius = {
|
|
|
|
|
let pad = f32::from(space_xxxs) / 2.0;
|
|
|
|
|
cosmic
|
|
|
|
|
.radius_s()
|
|
|
|
|
.map(|r| if r > 0.0 { r + pad } else { 0.0 })
|
|
|
|
|
.into()
|
|
|
|
|
};
|
2024-01-17 16:09:56 +01:00
|
|
|
let pane_grid = PaneGrid::new(&self.pane_model.panes, |pane, tab_model, _is_maximized| {
|
|
|
|
|
let mut tab_column = widget::column::with_capacity(1);
|
|
|
|
|
|
2026-05-24 10:27:32 +02:00
|
|
|
tab_column = tab_column.push(
|
|
|
|
|
widget::container(
|
|
|
|
|
widget::tab_bar::horizontal(tab_model)
|
|
|
|
|
.enable_tab_drag(String::from("x-cosmic-term/tab"))
|
|
|
|
|
.on_reorder(move |event| Message::ReorderTab(pane, event))
|
|
|
|
|
.tab_drag_threshold(25.)
|
|
|
|
|
.button_height(32)
|
|
|
|
|
.button_spacing(space_xxs)
|
|
|
|
|
.on_activate(Message::TabActivate)
|
|
|
|
|
.on_close(|entity| Message::TabClose(Some(entity)))
|
fix(tabs): renommer l'onglet du bon panneau en multi-panneaux
Le renommage d'onglet visait le panneau actif au moment de la validation
et non celui ou le renommage avait ete ouvert. L'ouverture appelle
unfocus_all_terminals(), si bien que `pane_model.active()` pouvait
designer un autre panneau au moment du commit : en configuration
scindee, le nouveau titre atterrissait sur l'onglet d'un panneau voisin.
`rename_tab` memorise desormais le panneau : (Pane, Entity, String) au
lieu de (Entity, String). L'ouverture le capture via focused(), et
commit, annulation et remise a zero le retrouvent par
panes.get(pane) au lieu de active_mut(). Le focus est rendu au panneau
d'origine via set_focus(pane) plutot que par update_terminal_focus(),
qui le laissait au petit bonheur.
Reprend le stash de 2026-06-13, fait sur 39d3d01. Le seul conflit
portait sur dialog(), ou la branche a gagne depuis le dialogue de
chargement de disposition : les deux sont conserves.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 19:39:48 +02:00
|
|
|
.on_double_click(move |entity| Message::TabRenameOpenForPane(pane, entity))
|
2026-05-24 10:27:32 +02:00
|
|
|
.on_context(move |entity| Message::TabRenameOpenForPane(pane, entity)),
|
|
|
|
|
)
|
|
|
|
|
.class(style::Container::Custom(Box::new(|theme| {
|
|
|
|
|
let cosmic = theme.cosmic();
|
|
|
|
|
cosmic::iced::widget::container::Style {
|
2026-07-03 14:04:49 +02:00
|
|
|
icon_color: Some(Color::from(cosmic.background(false).on)),
|
|
|
|
|
text_color: Some(Color::from(cosmic.background(false).on)),
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
background: Some(iced::Background::Color(
|
|
|
|
|
cosmic.background(false).base.into(),
|
|
|
|
|
)),
|
2026-05-24 10:27:32 +02:00
|
|
|
border: iced::Border::default(),
|
|
|
|
|
shadow: iced::Shadow::default(),
|
|
|
|
|
snap: true,
|
|
|
|
|
}
|
|
|
|
|
})))
|
|
|
|
|
.width(Length::Fill),
|
|
|
|
|
);
|
2023-12-20 13:31:10 -07:00
|
|
|
|
2024-01-17 16:09:56 +01:00
|
|
|
let entity = tab_model.active();
|
2024-01-21 21:13:13 +01:00
|
|
|
let entity_middle_click = tab_model.active();
|
2024-01-17 16:09:56 +01:00
|
|
|
let terminal_id = self
|
|
|
|
|
.terminal_ids
|
|
|
|
|
.get(&pane)
|
|
|
|
|
.cloned()
|
|
|
|
|
.unwrap_or_else(widget::Id::unique);
|
2024-04-14 20:58:19 +01:00
|
|
|
if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
|
2025-12-29 20:42:52 -08:00
|
|
|
let mut terminal_box = terminal_box(terminal, &self.key_binds)
|
2024-04-14 20:58:19 +01:00
|
|
|
.id(terminal_id)
|
2026-05-24 10:27:32 +02:00
|
|
|
.disabled(self.core.window.show_context || self.rename_tab.is_some())
|
2025-10-16 09:27:00 +02:00
|
|
|
.on_context_menu(move |menu_state| Message::TabContextMenu(pane, menu_state))
|
2024-04-26 10:27:52 -06:00
|
|
|
.on_middle_click(move || Message::MiddleClick(pane, Some(entity_middle_click)))
|
2024-11-17 01:47:50 +01:00
|
|
|
.on_open_hyperlink(Some(Box::new(Message::LaunchUrl)))
|
2025-01-21 17:07:45 +01:00
|
|
|
.on_window_focused(|| Message::WindowFocused)
|
|
|
|
|
.on_window_unfocused(|| Message::WindowUnfocused)
|
2026-07-06 09:26:53 -04:00
|
|
|
.opacity(if t.transparent {
|
|
|
|
|
t.cosmic().alpha_map.blurred_alpha(t.cosmic().frosted)
|
|
|
|
|
} else {
|
|
|
|
|
self.config.opacity_ratio()
|
|
|
|
|
})
|
2024-11-06 18:44:38 +01:00
|
|
|
.padding(space_xxs)
|
2025-10-06 15:02:34 -04:00
|
|
|
.sharp_corners(self.core.window.sharp_corners)
|
2026-04-06 18:38:10 -06:00
|
|
|
.show_headerbar(self.config.show_headerbar)
|
2026-07-29 21:51:03 +05:30
|
|
|
.pane_border_radius(show_pane_borders.then_some(pane_corner_radius))
|
|
|
|
|
.border(pane_border(cosmic, t.transparent, show_pane_borders));
|
2024-04-14 20:58:19 +01:00
|
|
|
|
|
|
|
|
if self.config.focus_follow_mouse {
|
|
|
|
|
terminal_box = terminal_box.on_mouse_enter(move || Message::MouseEnter(pane));
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-01 14:38:17 +02:00
|
|
|
if self.config.copy_on_select_clipboard {
|
|
|
|
|
terminal_box =
|
|
|
|
|
terminal_box.on_selection_done(move || Message::SelectionDone(pane, entity));
|
|
|
|
|
}
|
|
|
|
|
|
yoda: attenuation des panneaux non focalises, a la Ghostty
Avec Claude Code et Modjo cote a cote, rien ne distinguait le panneau ou
l'on tape, hors le curseur creux. L'amont assombrissait le fond de 8 %
(aussi quand la fenetre perdait le focus) et l'a retire en juillet
(a43b5ae).
Reprise de unfocused-split-opacity de Ghostty : un voile de la couleur du
fond, texte compris, sur chaque panneau non focalise, seulement quand il y
en a plusieurs ; la perte de focus de la fenetre ne change rien.
- Config unfocused_pane_dim (pourcentage, defaut 30 = opacite 0,7 de
Ghostty, 0 desactive, plafond 85 = opacite minimale 0,15 de Ghostty).
- Reglages > Panneaux : curseur « Attenuation des panneaux inactifs ».
- terminal_box : voile trace dans un calque a part en fin de rendu (dans
le meme calque, la primitive de texte passerait par-dessus), aux coins
du fond du panneau.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:40:47 +02:00
|
|
|
// [yoda] Voile sur les panneaux non focalisés (à la Ghostty), seulement
|
|
|
|
|
// quand il y en a plusieurs : on voit d'un coup d'œil où l'on tape.
|
|
|
|
|
if let Some(dim) = self.config.unfocused_pane_dim_ratio()
|
|
|
|
|
&& self.pane_model.panes.panes.len() > 1
|
|
|
|
|
&& pane != self.pane_model.focused()
|
|
|
|
|
{
|
|
|
|
|
terminal_box = terminal_box.dim(dim);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-08 13:49:17 -06:00
|
|
|
// 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 mut context_menu = widget::context_menu(
|
|
|
|
|
terminal_box,
|
|
|
|
|
context_link.map(|link| {
|
2026-09-24 20:52:15 +02:00
|
|
|
menu::context_menu(
|
|
|
|
|
&self.config,
|
|
|
|
|
&self.key_binds,
|
|
|
|
|
entity,
|
|
|
|
|
link,
|
|
|
|
|
self.terminal_monitors(pane, entity),
|
yoda: lecture seule par onglet, a la Ghostty (toggle_readonly)
Surveiller une session Claude Code ou Modjo sans risquer de lui taper
dedans, ni de lui coller quoi que ce soit.
- Terminal::read_only : input_no_scroll, input_scroll (donc paste) et
la molette en mode souris n'ecrivent plus rien, et une frappe ne ramene
plus la vue en bas. Couvre touches legacy et kitty, collage, clic
milieu, Ctrl+C, gestionnaire de mots de passe, IME, glisser-deposer.
- Terminal::respond : ecriture non bloquee pour ce que le terminal repond
lui-meme (PtyWrite, couleurs, taille, rapports de focus), sinon une
application attendrait une reponse qui ne vient pas (CSI ? u de Claude
Code, DA, DSR...).
- terminal_box : en lecture seule la souris revient au terminal
(selection, copie et defilement locaux, meme dans une application qui
la capture).
- Case « Lecture seule » dans Affichage et dans le menu contextuel ;
action ToggleReadOnly dans les raccourcis (groupe Autre, sans
raccourci par defaut, comme Ghostty et Konsole) ; cadenas
changes-prevent-symbolic sur l'onglet, apres les alertes.
Test de bout en bout dans un vrai PTY (cat) : saisie et collage bloques,
reponse transmise, saisie retablie ; il echoue si le verrou est retire.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 14:19:02 +02:00
|
|
|
self.terminal_read_only(pane, entity),
|
2026-09-24 20:52:15 +02:00
|
|
|
)
|
2026-09-08 13:49:17 -06:00
|
|
|
}),
|
|
|
|
|
)
|
|
|
|
|
.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);
|
2026-03-18 11:13:08 -06:00
|
|
|
}
|
2026-09-08 13:49:17 -06:00
|
|
|
tab_column = tab_column.push(context_menu);
|
2024-01-17 16:09:56 +01:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
|
2024-02-02 15:16:43 +01:00
|
|
|
//Only draw find in the currently focused pane
|
2025-01-21 17:07:45 +01:00
|
|
|
if self.find && pane == self.pane_model.focused() {
|
2024-01-17 16:09:56 +01:00
|
|
|
let find_input = widget::text_input::text_input(
|
|
|
|
|
fl!("find-placeholder"),
|
|
|
|
|
&self.find_search_value,
|
|
|
|
|
)
|
|
|
|
|
.id(self.find_search_id.clone())
|
|
|
|
|
.on_input(Message::FindSearchValueChanged)
|
|
|
|
|
// This is inverted for ease of use, usually in terminals you want to search
|
|
|
|
|
// upwards, which is FindPrevious
|
2025-03-19 12:27:00 -04:00
|
|
|
.on_submit(|_| {
|
|
|
|
|
if self.modifiers.contains(Modifiers::SHIFT) {
|
|
|
|
|
Message::FindNext
|
|
|
|
|
} else {
|
|
|
|
|
Message::FindPrevious
|
|
|
|
|
}
|
2024-01-17 16:09:56 +01:00
|
|
|
})
|
|
|
|
|
.width(Length::Fixed(320.0))
|
|
|
|
|
.trailing_icon(
|
2024-09-20 19:44:36 -06:00
|
|
|
button::custom(icon_cache_get("edit-clear-symbolic", 16))
|
2024-01-17 16:09:56 +01:00
|
|
|
.on_press(Message::FindSearchValueChanged(String::new()))
|
2024-10-22 12:31:02 -06:00
|
|
|
.class(style::Button::Icon)
|
2024-01-17 16:09:56 +01:00
|
|
|
.into(),
|
|
|
|
|
);
|
|
|
|
|
let find_widget = widget::row::with_children(vec![
|
|
|
|
|
find_input.into(),
|
|
|
|
|
widget::tooltip(
|
2024-09-20 19:44:36 -06:00
|
|
|
button::custom(icon_cache_get("go-up-symbolic", 16))
|
2024-01-17 16:09:56 +01:00
|
|
|
.on_press(Message::FindPrevious)
|
|
|
|
|
.padding(space_xxs)
|
2024-10-22 12:31:02 -06:00
|
|
|
.class(style::Button::Icon),
|
|
|
|
|
widget::text::body(fl!("find-previous")),
|
2024-01-17 16:09:56 +01:00
|
|
|
widget::tooltip::Position::Top,
|
2023-12-22 15:00:50 -07:00
|
|
|
)
|
|
|
|
|
.into(),
|
2024-01-17 16:09:56 +01:00
|
|
|
widget::tooltip(
|
2024-09-20 19:44:36 -06:00
|
|
|
button::custom(icon_cache_get("go-down-symbolic", 16))
|
2024-01-17 16:09:56 +01:00
|
|
|
.on_press(Message::FindNext)
|
|
|
|
|
.padding(space_xxs)
|
2024-10-22 12:31:02 -06:00
|
|
|
.class(style::Button::Icon),
|
|
|
|
|
widget::text::body(fl!("find-next")),
|
2024-01-17 16:09:56 +01:00
|
|
|
widget::tooltip::Position::Top,
|
|
|
|
|
)
|
|
|
|
|
.into(),
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
widget::checkbox(self.find_case_sensitive)
|
|
|
|
|
.label(fl!("find-case-sensitive"))
|
|
|
|
|
.on_toggle(Message::FindCaseSensitive)
|
|
|
|
|
.into(),
|
|
|
|
|
widget::checkbox(self.find_regex)
|
|
|
|
|
.label(fl!("find-regex"))
|
|
|
|
|
.on_toggle(Message::FindRegex)
|
|
|
|
|
.into(),
|
2026-03-18 12:21:57 -04:00
|
|
|
widget::space::horizontal().into(),
|
2024-09-20 19:44:36 -06:00
|
|
|
button::custom(icon_cache_get("window-close-symbolic", 16))
|
2024-01-17 16:09:56 +01:00
|
|
|
.on_press(Message::Find(false))
|
|
|
|
|
.padding(space_xxs)
|
2024-10-22 12:31:02 -06:00
|
|
|
.class(style::Button::Icon)
|
2024-01-17 16:09:56 +01:00
|
|
|
.into(),
|
|
|
|
|
])
|
2024-10-22 12:31:02 -06:00
|
|
|
.align_y(Alignment::Center)
|
2024-01-17 16:09:56 +01:00
|
|
|
.padding(space_xxs)
|
|
|
|
|
.spacing(space_xxs);
|
|
|
|
|
|
2024-03-18 15:05:53 -06:00
|
|
|
tab_column = tab_column
|
|
|
|
|
.push(widget::layer_container(find_widget).layer(cosmic_theme::Layer::Primary));
|
2024-04-14 20:58:19 +01:00
|
|
|
} else {
|
|
|
|
|
// TODO
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
2023-12-17 17:49:39 -07:00
|
|
|
|
2024-08-03 20:33:55 -06:00
|
|
|
DndDestination::for_data::<DndDrop>(tab_column, move |data, action| {
|
|
|
|
|
if let Some(data) = data {
|
|
|
|
|
if action == DndAction::Move {
|
|
|
|
|
Message::Drop(Some((pane, entity, data)))
|
|
|
|
|
} else {
|
|
|
|
|
log::warn!("unsuppported action: {:?}", action);
|
|
|
|
|
Message::Drop(None)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
Message::Drop(None)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
.apply(pane_grid::Content::new)
|
2024-01-17 16:09:56 +01:00
|
|
|
})
|
|
|
|
|
.width(Length::Fill)
|
|
|
|
|
.height(Length::Fill)
|
|
|
|
|
.on_click(Message::PaneClicked)
|
|
|
|
|
.on_resize(space_xxs, Message::PaneResized)
|
|
|
|
|
.on_drag(Message::PaneDragged);
|
|
|
|
|
|
2024-02-12 12:36:30 -07:00
|
|
|
//TODO: apply window border radius xs at bottom of window
|
2026-04-06 18:38:10 -06:00
|
|
|
if show_pane_borders {
|
2026-07-29 21:51:03 +05:30
|
|
|
// Each pane draws its own border stroke. Painting a filled
|
|
|
|
|
// container behind the grid instead would stack its alpha with the
|
|
|
|
|
// translucent panes and wash out the blurred backdrop.
|
|
|
|
|
pane_grid.spacing(space_xxxs).into()
|
2026-04-06 18:38:10 -06:00
|
|
|
} else {
|
|
|
|
|
pane_grid.into()
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
|
2025-06-23 11:40:43 -06:00
|
|
|
fn system_theme_update(
|
|
|
|
|
&mut self,
|
|
|
|
|
_keys: &[&'static str],
|
2025-09-10 14:33:46 +02:00
|
|
|
_new_theme: &cosmic::cosmic_theme::Theme,
|
2025-06-23 11:40:43 -06:00
|
|
|
) -> Task<Self::Message> {
|
|
|
|
|
self.update(Message::SystemThemeChange)
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
fn subscription(&self) -> Subscription<Self::Message> {
|
2023-12-22 14:31:01 -07:00
|
|
|
struct ConfigSubscription;
|
|
|
|
|
struct TerminalEventSubscription;
|
|
|
|
|
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
let mut subscriptions = vec![
|
2024-10-22 12:31:02 -06:00
|
|
|
event::listen_with(|event, _status, _window_id| match event {
|
2026-05-26 15:40:59 -04:00
|
|
|
Event::Keyboard(KeyEvent::KeyPressed {
|
|
|
|
|
key,
|
|
|
|
|
physical_key,
|
|
|
|
|
modifiers,
|
|
|
|
|
..
|
|
|
|
|
}) => Some(Message::Key(modifiers, physical_key, key)),
|
2024-01-11 13:20:16 -07:00
|
|
|
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
|
|
|
|
|
Some(Message::Modifiers(modifiers))
|
|
|
|
|
}
|
2024-01-21 21:13:13 +01:00
|
|
|
Event::Mouse(MouseEvent::ButtonReleased(MouseButton::Left)) => {
|
|
|
|
|
Some(Message::CopyPrimary(None))
|
|
|
|
|
}
|
2023-12-21 22:13:17 -07:00
|
|
|
_ => None,
|
|
|
|
|
}),
|
2026-03-18 12:21:57 -04:00
|
|
|
Subscription::run_with(TypeId::of::<TerminalEventSubscription>(), |_| {
|
|
|
|
|
stream::channel(
|
|
|
|
|
100,
|
|
|
|
|
|mut output: iced::futures::channel::mpsc::Sender<Message>| async move {
|
2026-05-18 16:30:51 +02:00
|
|
|
let (event_tx, mut event_rx) = mpsc::unbounded_channel();
|
2026-03-18 12:21:57 -04:00
|
|
|
output.send(Message::TermEventTx(event_tx)).await.unwrap();
|
|
|
|
|
|
|
|
|
|
while let Some((pane, entity, event)) = event_rx.recv().await {
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
let message = match event {
|
|
|
|
|
PtyEvent::Term(event) => Message::TermEvent(pane, entity, event),
|
|
|
|
|
PtyEvent::Osc(event) => Message::TermOsc(pane, entity, event),
|
|
|
|
|
};
|
|
|
|
|
output.send(message).await.unwrap();
|
2026-03-18 12:21:57 -04:00
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
|
2026-03-18 12:21:57 -04:00
|
|
|
panic!("terminal event channel closed");
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}),
|
yoda: notifications de bureau OSC 9/777 et cloche utile, a la Ghostty
vte 0.15 jette les OSC 9 et 777 : Claude Code (canal « ghostty ») ou tout
programme qui veut signaler « j'attends » ne pouvait rien faire remonter,
et la cloche etait un TODO. Avec plusieurs sessions en onglets, rien ne
disait laquelle attendait une reponse.
- osc_tap : le Pty est enveloppe, son lecteur passe chaque lecture a un
petit automate qui repere OSC 777;notify;titre;corps et OSC 9;texte
(les commandes ConEmu 9;N, dont la progression 9;4, sont ecartees comme
dans Ghostty). Ni alacritty_terminal ni vte ne sont forkes.
- desktop_notify : org.freedesktop.Notifications en direct (zbus), pour
recuperer le jeton ActivationToken au clic : la notification ramene la
fenetre au premier plan, sur le bon panneau et le bon onglet. Une
notification par onglet au plus (replaces_id), anti-rafale de Ghostty
(1/s, identique toutes les 5 s), titre suivi du nom de l'onglet.
- Rien n'est envoye pour l'onglet qu'on a sous les yeux. Sinon l'onglet
porte une icone d'attention (cloche ou notification), eteinte quand il
reprend le focus.
- Cloche : indicateur d'onglet seulement. Pas de demande d'attention
xdg-activation : cosmic-comp donne un jeton valide a toute application
non sandboxee, la fenetre volerait le focus.
- Focus 1004 : l'onglet quitte recoit enfin son focus-out (seul l'onglet
actif de chaque panneau etait mis a jour) ; un agent en arriere-plan ne
croit plus qu'on le regarde.
- Chevrons du corps remplaces par ‹ › : cosmic-notifications lit le corps
en HTML sans decoder les entites.
7 tests, dont un de bout en bout dans un vrai PTY.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 13:12:28 +02:00
|
|
|
desktop_notify::subscription().map(Message::DesktopNotify),
|
2023-12-22 14:31:01 -07:00
|
|
|
cosmic_config::config_subscription(
|
|
|
|
|
TypeId::of::<ConfigSubscription>(),
|
|
|
|
|
Self::APP_ID.into(),
|
|
|
|
|
CONFIG_VERSION,
|
|
|
|
|
)
|
2024-01-19 09:46:06 -07:00
|
|
|
.map(|update| {
|
|
|
|
|
if !update.errors.is_empty() {
|
|
|
|
|
log::debug!(
|
|
|
|
|
"errors loading config {:?}: {:?}",
|
|
|
|
|
update.keys,
|
|
|
|
|
update.errors
|
|
|
|
|
);
|
2023-12-22 14:31:01 -07:00
|
|
|
}
|
2026-04-10 19:00:25 +02:00
|
|
|
Message::Config(Box::new(update.config))
|
2023-12-22 14:31:01 -07:00
|
|
|
}),
|
2024-02-22 09:55:46 -07:00
|
|
|
match &self.dialog_opt {
|
|
|
|
|
Some(dialog) => dialog.subscription(),
|
2024-10-22 12:31:02 -06:00
|
|
|
None => Subscription::none(),
|
2024-02-22 09:55:46 -07:00
|
|
|
},
|
feat: Konsole parity phase 1 (save output, search options, monitors, profile CLI)
Implements the first batch of COSMIC_TERMINAL_KONSOLE_PARITY.md:
Save output as text:
- New SaveOutput action in the Edit menu, terminal context menu and
keyboard shortcuts (Ctrl+Shift+S, Konsole parity, rebindable).
- Terminal::scrollback_text() extracts history plus visible screen via
alacritty bounds_to_string, trimming trailing empty lines.
- Save-file dialog through cosmic-files; the file is written in a
spawn_blocking task so large scrollbacks never stall the UI.
Search options:
- Case-sensitive and regex checkboxes in the find bar.
- App::find_pattern() escapes the pattern when regex mode is off and
prefixes (?i) when case-insensitive (same approach as Alacritty).
Per-tab monitors (activity / silence / process finished):
- Toggles in the View menu and terminal context menu, state stored in
Terminal (monitor_* fields).
- Activity alerts on PTY output (Wakeup) for non-active tabs; silence
alerts after 10 s without output (Konsole default); process-finished
compares the shell pgrp with the tty tpgid via /proc/<pid>/stat and
alerts when the foreground job exits.
- Silence and process checks run on a 1 s iced::time subscription that
is only active while at least one tab needs it.
- Tab bar shows an armed icon (view-reveal-symbolic) and per-kind alert
icons; activating the tab acknowledges the alert.
Profile CLI:
- --profile/-p <name-or-id> applies a profile to the first tab only,
unknown profiles exit(1) with the available list on stderr.
- --list-profiles prints "id<TAB>name" and exits.
- Config is now loaded before the daemonize fork so CLI output reaches
the launching terminal.
- --help now documents -e/--command, --no-daemon and the new flags
(-e already worked; audit confirmed everything after it becomes the
command and its arguments).
i18n: new en/fr strings (save-output, monitor-*, find-case-sensitive,
find-regex).
Validated with ./check_cosmic_local.sh terminal, cargo fmt, and a debug
build exercising the CLI flags and a 6 s live run. The shutdown panic
in iced_winit ("async fn resumed after completion") pre-exists and is
reproducible with the installed binary.
Leyoda 2026 – GPLv3
2026-07-06 09:50:58 +02:00
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Timer driving silence and process-finished monitors
|
|
|
|
|
if self.any_monitor_needs_tick() {
|
|
|
|
|
subscriptions.push(
|
|
|
|
|
iced::time::every(std::time::Duration::from_secs(1)).map(|_| Message::MonitorTick),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Subscription::batch(subscriptions)
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-06 16:21:26 -06:00
|
|
|
|
2026-07-29 21:51:03 +05:30
|
|
|
/// Divider color painted behind the pane grid to form pane borders.
|
|
|
|
|
///
|
|
|
|
|
/// When blur is active the transparent container must be used, otherwise the
|
|
|
|
|
/// fill is opaque and hides the blurred desktop behind the window.
|
|
|
|
|
fn pane_divider_color(
|
|
|
|
|
cosmic: &cosmic_theme::Theme,
|
|
|
|
|
transparent: bool,
|
|
|
|
|
) -> cosmic::cosmic_theme::palette::Srgba {
|
|
|
|
|
cosmic.background(transparent).divider
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Border stroke drawn around each terminal pane.
|
|
|
|
|
///
|
|
|
|
|
/// The stroke sits on the pane edge rather than behind it, so the pane
|
|
|
|
|
/// interior keeps its own opacity and the blurred desktop stays visible.
|
|
|
|
|
fn pane_border(cosmic: &cosmic_theme::Theme, transparent: bool, show: bool) -> iced::Border {
|
|
|
|
|
if show {
|
|
|
|
|
iced::Border {
|
|
|
|
|
color: Color::from(pane_divider_color(cosmic, transparent)),
|
|
|
|
|
width: 1.0,
|
|
|
|
|
..Default::default()
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
iced::Border::default()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::{pane_border, pane_divider_color};
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pane_border_is_drawn_only_when_enabled() {
|
|
|
|
|
// With borders off the pane must render exactly as before, so the
|
|
|
|
|
// stroke has to be zero width rather than a transparent hairline.
|
|
|
|
|
let theme = cosmic::cosmic_theme::Theme::dark_default();
|
|
|
|
|
assert_eq!(pane_border(&theme, true, false).width, 0.0);
|
|
|
|
|
assert!(pane_border(&theme, true, true).width > 0.0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pane_border_uses_the_divider_color() {
|
|
|
|
|
// The stroke is the only thing marking the pane edge now that nothing
|
|
|
|
|
// is painted behind the grid, so it must match the theme divider.
|
|
|
|
|
let theme = cosmic::cosmic_theme::Theme::dark_default();
|
|
|
|
|
let expected = cosmic::iced::Color::from(pane_divider_color(&theme, true));
|
|
|
|
|
assert_eq!(pane_border(&theme, true, true).color, expected);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pane_divider_is_translucent_when_blur_is_active() {
|
|
|
|
|
// The divider is painted as a full-window container behind the pane
|
|
|
|
|
// grid. With blur active it must keep alpha < 1 so the frosted
|
|
|
|
|
// backdrop still shows through instead of a flat opaque fill.
|
|
|
|
|
let theme = cosmic::cosmic_theme::Theme::dark_default();
|
|
|
|
|
let color = pane_divider_color(&theme, true);
|
|
|
|
|
assert!(
|
|
|
|
|
color.alpha < 1.0,
|
|
|
|
|
"expected translucent divider, got alpha {}",
|
|
|
|
|
color.alpha
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn pane_divider_is_opaque_without_blur() {
|
|
|
|
|
// Without blur there is nothing to show through, so the divider keeps
|
|
|
|
|
// the fully opaque background color.
|
|
|
|
|
let theme = cosmic::cosmic_theme::Theme::dark_default();
|
|
|
|
|
let color = pane_divider_color(&theme, false);
|
|
|
|
|
assert!(
|
|
|
|
|
(color.alpha - 1.0).abs() < f32::EPSILON,
|
|
|
|
|
"expected opaque divider, got alpha {}",
|
|
|
|
|
color.alpha
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|