Merge branch 'master' into master

This commit is contained in:
Andrea Coronese 2024-05-01 23:46:30 +02:00 committed by GitHub
commit 3ce7bd56ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 1446 additions and 791 deletions

View file

@ -0,0 +1,19 @@
name: "Publish every Git push to master to FlakeHub"
on:
push:
branches:
- "master"
jobs:
flakehub-publish:
runs-on: "ubuntu-latest"
permissions:
id-token: "write"
contents: "read"
steps:
- uses: "actions/checkout@v3"
- uses: "DeterminateSystems/nix-installer-action@main"
- uses: "DeterminateSystems/flakehub-push@main"
with:
name: "pop-os/cosmic-term"
rolling: true
visibility: "public"

976
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@ rust-version = "1.71"
vergen = { version = "8", features = ["git", "gitcl"] } vergen = { version = "8", features = ["git", "gitcl"] }
[dependencies] [dependencies]
alacritty_terminal = "0.20" alacritty_terminal = "0.23"
env_logger = "0.10" env_logger = "0.10"
hex_color = { version = "3", features = ["serde"] } hex_color = { version = "3", features = ["serde"] }
indexmap = "2" indexmap = "2"
@ -21,7 +21,8 @@ open = "5.0.2"
palette = { version = "0.7", features = ["serde"] } palette = { version = "0.7", features = ["serde"] }
paste = "1.0" paste = "1.0"
ron = "0.8" ron = "0.8"
serde = { version = "1", features = ["serde_derive"] } #TODO: downgrading serde for better compatibility with older rust
serde = { version = "=1.0.197", features = ["serde_derive"] }
shlex = "1" shlex = "1"
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
# Internationalization # Internationalization

View file

@ -1,5 +1,5 @@
( (
name: "gruvbox-dark", name: "Gruvbox Dark",
foreground: "#EBDBB2", foreground: "#EBDBB2",
background: "#282828", background: "#282828",
cursor: "#EBDBB2", cursor: "#EBDBB2",
@ -35,4 +35,4 @@
cyan: "#547055", cyan: "#547055",
white: "#747474", white: "#747474",
), ),
) )

1
debian/control vendored
View file

@ -4,6 +4,7 @@ Priority: optional
Maintainer: Jeremy Soller <jeremy@system76.com> Maintainer: Jeremy Soller <jeremy@system76.com>
Build-Depends: Build-Depends:
debhelper-compat (=13), debhelper-compat (=13),
git,
just (>= 1.13.0), just (>= 1.13.0),
pkg-config, pkg-config,
rust-all, rust-all,

96
i18n/de/cosmic_term.ftl Normal file
View file

@ -0,0 +1,96 @@
cosmic-terminal = COSMIC Terminal
new-terminal = Neues Terminal
# Context Pages
## About
git-description = Git commit {$hash} vom {$date}
## Color schemes
color-schemes = Farbschemen
rename = Umbenennen
export = Exportieren
delete = Löschen
import = Importieren
import-errors = Importfehler
## Profiles
profiles = Profile
name = Name
command-line = Startbefehl
tab-title = Überschrift
tab-title-description = Standardtitel des Tabs überschreiben
add-profile = Profil hinzufügen
new-profile = Neues Profil
make-default = Als Standard setzen
## Settings
settings = Einstellungen
### Appearance
appearance = Aussehen
theme = Thema
match-desktop = An System anpassen
dark = Dunkel
light = Hell
syntax-dark = Dunkles Farbschema
syntax-light = Helles Farbschema
default-zoom-step = Zoomstufen
opacity = Deckkraft des Hintergrundes
### Font
font = Schrift
advanced-font-settings = Fortgeschrittene Schrifteinstellungen
default-font = Schriftart
default-font-size = Schriftgröße
default-font-stretch = Schriftbreite
default-font-weight = Normale Schriftstärke
default-dim-font-weight = Matte Schriftstärke
default-bold-font-weight = Fette Schriftstärke
use-bright-bold = Fetten Text heller darstellen
### Splits
splits = Aufteilungen
focus-follow-mouse = Tippen folgt Maus
### Advanced
advanced = Fortgeschritten
show-headerbar = Kopfzeile anzeigen
show-header-description = Kopfzeile kann via Rechtsklickmenü angezeigt werden
# Find
find-placeholder = Suche...
find-previous = Vorheriges
find-next = Nächstes
# Menu
## File
file = Datei
new-tab = Neuer Tab
new-window = Neues Fenster
profile = Profil
menu-profiles = Profile...
close-tab = Tab schließen
quit = Beenden
## Edit
edit = Bearbeiten
copy = Kopieren
paste = Einfügen
select-all = Alles auswählen
find = Suche
## View
view = Ansicht
zoom-in = Schrift vergrößern
zoom-reset = Standardschriftgröße
zoom-out = Schrift verkleinern
next-tab = Nächster Tab
previous-tab = Vorheriger Tab
split-horizontal = Horizontal aufteilen
split-vertical = Vertikal aufteilen
pane-toggle-maximize = Vollbild umschalten
menu-color-schemes = Farbthemen...
menu-settings = Einstellungen...
menu-about = Über COSMIC Terminal...

View file

@ -23,6 +23,9 @@ tab-title-description = Override the default tab title
add-profile = Add profile add-profile = Add profile
new-profile = New profile new-profile = New profile
make-default = Make default make-default = Make default
working-directory = Working directory
hold = Hold
remain-open = Remain open after child process exits.
## Settings ## Settings
settings = Settings settings = Settings

View file

@ -22,7 +22,7 @@ tab-title = Título de pestaña
tab-title-description = Cambiar el título por defecto de la pestaña tab-title-description = Cambiar el título por defecto de la pestaña
add-profile = Añadir perfil add-profile = Añadir perfil
new-profile = Nuevo perfil new-profile = Nuevo perfil
make-default = Impostar como defecto make-default = Establecer como predeterminado
## Settings ## Settings
settings = Ajustes settings = Ajustes
@ -31,22 +31,22 @@ settings = Ajustes
appearance = Apariencia appearance = Apariencia
theme = Tema theme = Tema
match-desktop = Automático match-desktop = Automático
dark = Obscuro dark = Oscuro
light = Claro light = Claro
syntax-dark = Esquema de color obscura syntax-dark = Esquema de color oscuro
syntax-light = Esquema de color clara syntax-light = Esquema de color claro
default-zoom-step = Pasos de zoom default-zoom-step = Escalas de zoom
opacity = Opacidad del fondo opacity = Opacidad de fondo
### Font ### Font
font = Fuente font = Fuente
advanced-font-settings = Ajustes avanzados del fuente advanced-font-settings = Ajustes avanzados de fuente
default-font = Fuente default-font = Fuente
default-font-size = Tamaño del fuente default-font-size = Tamaño de fuente
default-font-stretch = Anchura del fuente default-font-stretch = Anchura de fuente
default-font-weight = Peso del fuente normal default-font-weight = Peso de fuente normal
default-dim-font-weight = Peso del fuente delgado default-dim-font-weight = Peso de fuente delgado
default-bold-font-weight = Peso del fuente en negritas default-bold-font-weight = Peso de fuente en negritas
use-bright-bold = Mostrar negritas en colores claros use-bright-bold = Mostrar negritas en colores claros
### Splits ### Splits
@ -54,9 +54,9 @@ splits = Splits
focus-follow-mouse = Enfoque del tecleo sigue el ratón focus-follow-mouse = Enfoque del tecleo sigue el ratón
### Advanced ### Advanced
advanced = Advanzado advanced = Avanzado
show-headerbar = Mostrar encabezado show-headerbar = Mostrar encabezado
show-header-description = Mostrar el encabezado desde el menú del clic secundario. show-header-description = Mostrar encabezado desde el menú del clic secundario.
# Find # Find
find-placeholder = Buscar... find-placeholder = Buscar...
@ -68,7 +68,7 @@ find-next = Buscar siguiente
## File ## File
file = Archivo file = Archivo
new-tab = Nueva pestaña new-tab = Nueva pestaña
new-window = Ventana nueva new-window = Nueva ventana
profile = Perfil profile = Perfil
menu-profiles = Perfiles... menu-profiles = Perfiles...
close-tab = Cerrar pestaña close-tab = Cerrar pestaña
@ -84,7 +84,7 @@ find = Buscar
## View ## View
view = Vista view = Vista
zoom-in = Texto más grande zoom-in = Texto más grande
zoom-reset = Tamaño por defecto del fuente zoom-reset = Tamaño de fuente por defecto
zoom-out = Texto más pequeño zoom-out = Texto más pequeño
next-tab = Pestaña siguiente next-tab = Pestaña siguiente
previous-tab = Pestaña previa previous-tab = Pestaña previa

96
i18n/fr/cosmic_term.ftl Normal file
View file

@ -0,0 +1,96 @@
cosmic-terminal = Terminal COSMIC
new-terminal = Nouveau terminal
# Context Pages
## About
git-description = Git commit {$hash} le {$date}
## Color schemes
color-schemes = Palettes de couleurs
rename = Renommer
export = Exporter
delete = Supprimer
import = Importer
import-errors = Importer erreurs
## Profiles
profiles = Profils
name = Nom
command-line = Ligne de commande
tab-title = Titre de l'onglet
tab-title-description = Remplacer le titre d'onglet par défaut
add-profile = Ajouter profil
new-profile = Nouveau profil
make-default = Rendre par défaut
## Settings
settings = Paramètres
### Appearance
appearance = Apparence
theme = Thème
match-desktop = Assortir au bureau
dark = Sombre
light = Clair
syntax-dark = Palette de couleur sombre
syntax-light = Palette de couleur claire
default-zoom-step = Pas du zoom
opacity = Opacité de l'arrière-plan
### Font
font = Police
advanced-font-settings = Paramètres de police avancés
default-font = Police
default-font-size = Taille de la police
default-font-stretch = Étirement de la police
default-font-weight = Graisse de caractère normale
default-dim-font-weight = Graisse de caractère légère
default-bold-font-weight = Graisse de caractère grasse
use-bright-bold = Rendre le texte en gras plus clair
### Splits
splits = Divisions
focus-follow-mouse = Le focus de la saisie suit la souris
### Advanced
advanced = Avancé
show-headerbar = Afficher l'en-tête
show-header-description = Révéler l'en-tête du menu contextuel.
# Find
find-placeholder = Rechercher...
find-previous = Chercher précédent
find-next = Chercher suivant
# Menu
## File
file = Fichier
new-tab = Nouvel onglet
new-window = Nouvelle fenêtre
profile = Profil
menu-profiles = Profils...
close-tab = Fermer l'onglet
quit = Quitter
## Edit
edit = Modifier
copy = Copier
paste = Coller
select-all = Sélectionner tout
find = Rechercher
## View
view = Affichage
zoom-in = Texte plus grand
zoom-reset = Taille de texte par défaut
zoom-out = Texte plus petit
next-tab = Onglet suivant
previous-tab = Onglet précédent
split-horizontal = Diviser horizontalement
split-vertical = Diviser verticalement
pane-toggle-maximize = Maximiser l'affichage
menu-color-schemes = Palettes de couleurs...
menu-settings = Paramètres...
menu-about = À propos du terminal COSMIC...

View file

@ -18,7 +18,7 @@ import-errors = インポートエラー
profiles = プロファイル profiles = プロファイル
name = 名前 name = 名前
command-line = コマンドライン command-line = コマンドライン
tab-title = タブイトル tab-title = タブイトル
tab-title-description = デフォルトのタブタイトルを無効にします tab-title-description = デフォルトのタブタイトルを無効にします
add-profile = プロファイルを追加 add-profile = プロファイルを追加
new-profile = 新しいプロファイル new-profile = 新しいプロファイル

View file

@ -4,7 +4,7 @@ new-terminal = Nowy terminal
# Context Pages # Context Pages
## About ## About
git-description = Git commit {$hash} on {$date} git-description = Git commit {$hash} z {$date}
## Color schemes ## Color schemes
color-schemes = Schemat kolorów color-schemes = Schemat kolorów
@ -24,7 +24,6 @@ add-profile = Dodaj profil
new-profile = Nowy profil new-profile = Nowy profil
make-default = Uczyń domyślnym make-default = Uczyń domyślnym
## Settings ## Settings
settings = Ustawienia settings = Ustawienia
@ -70,6 +69,8 @@ find-next = Szukaj następny
file = Plik file = Plik
new-tab = Nowa karta new-tab = Nowa karta
new-window = Nowe okno new-window = Nowe okno
profile = Profil
menu-profiles = Profile...
close-tab = Zamknij kartę close-tab = Zamknij kartę
quit = Zamknij quit = Zamknij

View file

@ -1,5 +1,29 @@
cosmic-terminal = Терминал COSMIC
new-terminal = Новый терминал
# Context Pages # Context Pages
## About
git-description = Git-коммит {$hash} от {$date}
## Color schemes
color-schemes = Цветовые схемы
rename = Переименовать
export = Экспортировать
delete = Удалить
import = Импортировать
import-errors = Ошибки при импорте
## Profiles
profiles = Профили
name = Имя
command-line = Командная строка
tab-title = Заголовок вкладки
tab-title-description = Переопределить заголовок вкладки по умолчанию
add-profile = Добавить профиль
new-profile = Новый профиль
make-default = Установить по умолчанию
## Settings ## Settings
settings = Параметры settings = Параметры
@ -12,6 +36,7 @@ light = Светлая
syntax-dark = Цветовая схема темная syntax-dark = Цветовая схема темная
syntax-light = Цветовая схема светлая syntax-light = Цветовая схема светлая
default-zoom-step = Шаги масштабирования default-zoom-step = Шаги масштабирования
opacity = Прозрачность фона
### Font ### Font
font = Шрифт font = Шрифт
@ -44,6 +69,8 @@ find-next = Найти далее
file = Файл file = Файл
new-tab = Новая вкладка new-tab = Новая вкладка
new-window = Новое окно new-window = Новое окно
profile = Профиль
menu-profiles = Профили...
close-tab = Закрыть вкладку close-tab = Закрыть вкладку
quit = Завершить quit = Завершить
@ -64,4 +91,6 @@ previous-tab = Предыдущая вкладка
split-horizontal = Разделение по горизонтали split-horizontal = Разделение по горизонтали
split-vertical = Разделение по вертикали split-vertical = Разделение по вертикали
pane-toggle-maximize = Переключить на весь экран pane-toggle-maximize = Переключить на весь экран
menu-color-schemes = Цветовые схемы...
menu-settings = Параметры... menu-settings = Параметры...
menu-about = О Терминале COSMIC...

View file

@ -1,9 +1,33 @@
# Context sidor cosmic-terminal = COSMIC Terminal
new-terminal = Ny terminal
## Settings # Context Pages
## Om
git-description = Git commit {$hash} på {$date}
## Färgscheman
color-schemes = Färgscheman
rename = Byt namn
export = Exportera
delete = Ta bort
import = Importera
import-errors = Fel vid import
## Profiler
profiles = Profiler
name = Namn
command-line = Kommandorad
tab-title = Titel på flik
tab-title-description = Åsidosätt standardtitel för flik
add-profile = Lägg till profil
new-profile = Ny profil
make-default = Gör till standard
## Inställningar
settings = Inställningar settings = Inställningar
### Appearance ### Utseende
appearance = Utseende appearance = Utseende
theme = Tema theme = Tema
match-desktop = Matcha skrivbordet match-desktop = Matcha skrivbordet
@ -12,6 +36,7 @@ light = Ljust
syntax-dark = Färgschema mörkt syntax-dark = Färgschema mörkt
syntax-light = Färgschema ljust syntax-light = Färgschema ljust
default-zoom-step = Zoom steg default-zoom-step = Zoom steg
opacity = Bakgrundens opacitet
### Teckensnitt ### Teckensnitt
font = Teckensnitt font = Teckensnitt
@ -44,6 +69,8 @@ find-next = Hitta nästa
file = Fil file = Fil
new-tab = Ny flik new-tab = Ny flik
new-window = Nytt fönster new-window = Nytt fönster
profile = Profil
menu-profiles = Profiler…
close-tab = Stäng flik close-tab = Stäng flik
quit = Avsluta quit = Avsluta
@ -64,4 +91,6 @@ previous-tab = Föregående flik
split-horizontal = Dela horisontellt split-horizontal = Dela horisontellt
split-vertical = Dela vertikalt split-vertical = Dela vertikalt
pane-toggle-maximize = Växla maximerad pane-toggle-maximize = Växla maximerad
menu-color-schemes = Färgscheman…
menu-settings = Inställningar… menu-settings = Inställningar…
menu-about = Om COSMIC Terminal…

View file

@ -1,5 +1,19 @@
cosmic-terminal = COSMIC Uçbirim
new-terminal = Yeni uçbirim
# Context Pages # Context Pages
## About
git-description = Git commit {$hash}, {$date}
## Color schemes
color-schemes = Renk şemaları
rename = Yeniden adlandır
export = Dışa akar
delete = Sil
import = İçe aktar
import-errors = İçe aktarma hataları
## Profiles ## Profiles
profiles = Profiller profiles = Profiller
name = İsim name = İsim
@ -8,6 +22,7 @@ tab-title = Sekme başlığı
tab-title-description = Varsayılan sekme başlığını geçersiz kılar tab-title-description = Varsayılan sekme başlığını geçersiz kılar
add-profile = Profil ekle add-profile = Profil ekle
new-profile = Yeni profil new-profile = Yeni profil
make-default = Varsayılan yap
## Settings ## Settings
settings = Ayarlar settings = Ayarlar
@ -21,6 +36,7 @@ light = Aydınlık
syntax-dark = Karanlık renk şeması syntax-dark = Karanlık renk şeması
syntax-light = Aydınlık renk şeması syntax-light = Aydınlık renk şeması
default-zoom-step = Yakınlaştırma basamakları default-zoom-step = Yakınlaştırma basamakları
opacity = Arkaplan saydamlığı
### Font ### Font
font = Yazı tipi font = Yazı tipi
@ -74,5 +90,7 @@ next-tab = Sonraki sekme
previous-tab = Önceki sekme previous-tab = Önceki sekme
split-horizontal = Yatay böl split-horizontal = Yatay böl
split-vertical = Dikey böl split-vertical = Dikey böl
pane-toggle-maximize = En üste geç pane-toggle-maximize = Tam ekrana geç
menu-color-schemes = Renk şemaları...
menu-settings = Ayarlar... menu-settings = Ayarlar...
menu-about = COSMIC Uçbirim hakkında

View file

@ -0,0 +1,99 @@
cosmic-terminal = COSMIC 终端
new-terminal = 新建终端
# Context Pages
## About
git-description = Git 提交 {$hash} 于 {$date}
## Color schemes
color-schemes = 配色方案
rename = 重命名
export = 导出
delete = 删除
import = 导入
import-errors = 导入错误
## Profiles
profiles = 配置文件
name = 名称
command-line = 命令行
tab-title = 标签标题
tab-title-description = 覆盖默认标签标题
add-profile = 添加配置文件
new-profile = 新建配置文件
make-default = 设为默认配置
working-directory = 工作目录
hold = 保留
remain-open = 子进程结束后保持打开
## Settings
settings = 设置
### Appearance
appearance = 外观
theme = 主题
match-desktop = 匹配主题颜色
dark = 深色
light = 浅色
syntax-dark = 深色配色方案
syntax-light = 浅色配色方案
default-zoom-step = 缩放步长
opacity = 背景不透明度
### Font
font = 字体
advanced-font-settings = 高级字体设置
default-font = 默认字体
default-font-size = 字体大小
default-font-stretch = 字体延伸
default-font-weight = 普通字体粗细
default-dim-font-weight = 淡字体粗细
default-bold-font-weight = 粗字体粗细
use-bright-bold = 使粗体更亮
### Splits
splits = 终端分割
focus-follow-mouse = 聚焦窗口跟随鼠标
### Advanced
advanced = 高级
show-headerbar = 显示标题栏
show-header-description = 从右键菜单显示标题栏
# Find
find-placeholder = 查找...
find-previous = 上一个
find-next = 下一个
# Menu
## File
file = 文件
new-tab = 新建标签页
new-window = 新建窗口
profile = 配置文件
menu-profiles = 配置文件...
close-tab = 关闭标签页
quit = 退出
## Edit
edit = 编辑
copy = 复制
paste = 粘贴
select-all = 全选
find = 查找
## View
view = 视图
zoom-in = 放大文字
zoom-reset = 默认文字大小
zoom-out = 缩小文字
next-tab = 下一个标签页
previous-tab = 上一个标签页
split-horizontal = 水平分割
split-vertical = 垂直分割
pane-toggle-maximize = 切换最大化
menu-color-schemes = 配色方案...
menu-settings = 设置...
menu-about = 关于 COSMIC 终端...

View file

@ -14,6 +14,8 @@ use std::sync::OnceLock;
use crate::fl; use crate::fl;
pub const CONFIG_VERSION: u64 = 1; pub const CONFIG_VERSION: u64 = 1;
pub const COSMIC_THEME_DARK: &str = "COSMIC Dark";
pub const COSMIC_THEME_LIGHT: &str = "COSMIC Light";
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum AppTheme { pub enum AppTheme {
@ -186,6 +188,10 @@ pub struct Profile {
pub syntax_theme_light: String, pub syntax_theme_light: String,
#[serde(default)] #[serde(default)]
pub tab_title: String, pub tab_title: String,
#[serde(default)]
pub working_directory: String,
#[serde(default)]
pub hold: bool,
} }
impl Default for Profile { impl Default for Profile {
@ -193,9 +199,11 @@ impl Default for Profile {
Self { Self {
name: fl!("new-profile"), name: fl!("new-profile"),
command: String::new(), command: String::new(),
syntax_theme_dark: "COSMIC Dark".to_string(), syntax_theme_dark: COSMIC_THEME_DARK.to_string(),
syntax_theme_light: "COSMIC Light".to_string(), syntax_theme_light: COSMIC_THEME_LIGHT.to_string(),
tab_title: String::new(), tab_title: String::new(),
working_directory: String::new(),
hold: true,
} }
} }
} }
@ -239,8 +247,8 @@ impl Default for Config {
opacity: 100, opacity: 100,
profiles: BTreeMap::new(), profiles: BTreeMap::new(),
show_headerbar: true, show_headerbar: true,
syntax_theme_dark: "COSMIC Dark".to_string(), syntax_theme_dark: COSMIC_THEME_DARK.to_string(),
syntax_theme_light: "COSMIC Light".to_string(), syntax_theme_light: COSMIC_THEME_LIGHT.to_string(),
use_bright_bold: false, use_bright_bold: false,
default_profile: None, default_profile: None,
} }
@ -284,11 +292,11 @@ impl Config {
let color_schemes = self.color_schemes(color_scheme_kind); let color_schemes = self.color_schemes(color_scheme_kind);
let mut color_scheme_names = let mut color_scheme_names =
Vec::<(String, ColorSchemeId)>::with_capacity(color_schemes.len()); Vec::<(String, ColorSchemeId)>::with_capacity(color_schemes.len());
for (color_scheme_id, color_scheme) in color_schemes.iter() { for (color_scheme_id, color_scheme) in color_schemes {
let mut name = color_scheme.name.clone(); let mut name = color_scheme.name.clone();
let mut copies = 1; let mut copies = 1;
while color_scheme_names.iter().find(|x| x.0 == name).is_some() { while color_scheme_names.iter().any(|x| x.0 == name) {
copies += 1; copies += 1;
name = format!("{} ({})", color_scheme.name, copies); name = format!("{} ({})", color_scheme.name, copies);
} }
@ -314,17 +322,17 @@ impl Config {
} }
pub fn opacity_ratio(&self) -> f32 { pub fn opacity_ratio(&self) -> f32 {
(self.opacity as f32) / 100.0 f32::from(self.opacity) / 100.0
} }
// Get a sorted and adjusted for duplicates list of profile names and ids // Get a sorted and adjusted for duplicates list of profile names and ids
pub fn profile_names(&self) -> Vec<(String, ProfileId)> { pub fn profile_names(&self) -> Vec<(String, ProfileId)> {
let mut profile_names = Vec::<(String, ProfileId)>::with_capacity(self.profiles.len()); let mut profile_names = Vec::<(String, ProfileId)>::with_capacity(self.profiles.len());
for (profile_id, profile) in self.profiles.iter() { for (profile_id, profile) in &self.profiles {
let mut name = profile.name.clone(); let mut name = profile.name.clone();
let mut copies = 1; let mut copies = 1;
while profile_names.iter().find(|x| x.0 == name).is_some() { while profile_names.iter().any(|x| x.0 == name) {
copies += 1; copies += 1;
name = format!("{} ({})", profile.name, copies); name = format!("{} ({})", profile.name, copies);
} }

View file

@ -1,49 +1,9 @@
use cosmic::{ use cosmic::widget::menu::key_bind::{KeyBind, Modifier};
iced::keyboard::{Key, Modifiers}, use cosmic::{iced::keyboard::Key, iced_core::keyboard::key::Named};
iced_core::keyboard::key::Named, use std::collections::HashMap;
};
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt};
use crate::Action; use crate::Action;
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub enum Modifier {
Super,
Ctrl,
Alt,
Shift,
}
#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct KeyBind {
pub modifiers: Vec<Modifier>,
pub key: Key,
}
impl KeyBind {
pub fn matches(&self, modifiers: Modifiers, key: &Key) -> bool {
key == &self.key
&& modifiers.logo() == self.modifiers.contains(&Modifier::Super)
&& modifiers.control() == self.modifiers.contains(&Modifier::Ctrl)
&& modifiers.alt() == self.modifiers.contains(&Modifier::Alt)
&& modifiers.shift() == self.modifiers.contains(&Modifier::Shift)
}
}
impl fmt::Display for KeyBind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
for modifier in self.modifiers.iter() {
write!(f, "{:?} + ", modifier)?;
}
match &self.key {
Key::Character(c) => write!(f, "{}", c.to_uppercase()),
Key::Named(named) => write!(f, "{:?}", named),
other => write!(f, "{:?}", other),
}
}
}
//TODO: load from config //TODO: load from config
pub fn key_binds() -> HashMap<KeyBind, Action> { pub fn key_binds() -> HashMap<KeyBind, Action> {
let mut key_binds = HashMap::new(); let mut key_binds = HashMap::new();
@ -68,6 +28,7 @@ pub fn key_binds() -> HashMap<KeyBind, Action> {
bind!([Ctrl, Shift], Key::Character("Q".into()), WindowClose); bind!([Ctrl, Shift], Key::Character("Q".into()), WindowClose);
bind!([Ctrl, Shift], Key::Character("T".into()), TabNew); bind!([Ctrl, Shift], Key::Character("T".into()), TabNew);
bind!([Ctrl, Shift], Key::Character("V".into()), Paste); bind!([Ctrl, Shift], Key::Character("V".into()), Paste);
bind!([Shift], Key::Named(Named::Insert), PastePrimary);
bind!([Ctrl, Shift], Key::Character("W".into()), TabClose); bind!([Ctrl, Shift], Key::Character("W".into()), TabClose);
bind!([Ctrl], Key::Character(",".into()), Settings); bind!([Ctrl], Key::Character(",".into()), Settings);

View file

@ -43,6 +43,6 @@ pub fn localize() {
let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages(); let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages();
if let Err(error) = localizer.select(&requested_languages) { if let Err(error) = localizer.select(&requested_languages) {
eprintln!("Error while loading language for App List {}", error); eprintln!("Error while loading language for App List {error}");
} }
} }

View file

@ -2,6 +2,8 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use alacritty_terminal::{event::Event as TermEvent, term, term::color::Colors as TermColors, tty}; use alacritty_terminal::{event::Event as TermEvent, term, term::color::Colors as TermColors, tty};
use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::{ use cosmic::{
app::{message, Command, Core, Settings}, app::{message, Command, Core, Settings},
cosmic_config::{self, ConfigSet, CosmicConfigEntry}, cosmic_config::{self, ConfigSet, CosmicConfigEntry},
@ -12,6 +14,7 @@ use cosmic::{
clipboard, event, clipboard, event,
futures::SinkExt, futures::SinkExt,
keyboard::{Event as KeyEvent, Key, Modifiers}, keyboard::{Event as KeyEvent, Key, Modifiers},
mouse::{Button as MouseButton, Event as MouseEvent},
subscription::{self, Subscription}, subscription::{self, Subscription},
window, Alignment, Color, Event, Length, Limits, Padding, Point, window, Alignment, Color, Event, Length, Limits, Padding, Point,
}, },
@ -40,7 +43,7 @@ mod mouse_reporter;
use icon_cache::IconCache; use icon_cache::IconCache;
mod icon_cache; mod icon_cache;
use key_bind::{key_binds, KeyBind}; use key_bind::key_binds;
mod key_bind; mod key_bind;
mod localize; mod localize;
@ -145,13 +148,6 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut settings = Settings::default(); let mut settings = Settings::default();
settings = settings.theme(config.app_theme.theme()); settings = settings.theme(config.app_theme.theme());
#[cfg(target_os = "redox")]
{
// Redox does not support resize if doing CSDs
settings = settings.client_decorations(false);
}
settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0)); settings = settings.size_limits(Limits::NONE.min_width(360.0).min_height(180.0));
let flags = Flags { let flags = Flags {
@ -178,6 +174,7 @@ pub enum Action {
About, About,
ColorSchemes(ColorSchemeKind), ColorSchemes(ColorSchemeKind),
Copy, Copy,
CopyPrimary,
Find, Find,
PaneFocusDown, PaneFocusDown,
PaneFocusLeft, PaneFocusLeft,
@ -187,6 +184,7 @@ pub enum Action {
PaneSplitVertical, PaneSplitVertical,
PaneToggleMaximized, PaneToggleMaximized,
Paste, Paste,
PastePrimary,
ProfileOpen(ProfileId), ProfileOpen(ProfileId),
Profiles, Profiles,
SelectAll, SelectAll,
@ -212,46 +210,50 @@ pub enum Action {
ZoomReset, ZoomReset,
} }
impl Action { impl MenuAction for Action {
pub fn message(self, entity_opt: Option<segmented_button::Entity>) -> Message { type Message = Message;
fn message(&self, entity_opt: Option<segmented_button::Entity>) -> Message {
match self { match self {
Action::About => Message::ToggleContextPage(ContextPage::About), Self::About => Message::ToggleContextPage(ContextPage::About),
Action::ColorSchemes(color_scheme_kind) => { Self::ColorSchemes(color_scheme_kind) => {
Message::ToggleContextPage(ContextPage::ColorSchemes(color_scheme_kind)) Message::ToggleContextPage(ContextPage::ColorSchemes(*color_scheme_kind))
} }
Action::Copy => Message::Copy(entity_opt), Self::Copy => Message::Copy(entity_opt),
Action::Find => Message::Find(true), Self::CopyPrimary => Message::CopyPrimary(entity_opt),
Action::PaneFocusDown => Message::PaneFocusAdjacent(pane_grid::Direction::Down), Self::Find => Message::Find(true),
Action::PaneFocusLeft => Message::PaneFocusAdjacent(pane_grid::Direction::Left), Self::PaneFocusDown => Message::PaneFocusAdjacent(pane_grid::Direction::Down),
Action::PaneFocusRight => Message::PaneFocusAdjacent(pane_grid::Direction::Right), Self::PaneFocusLeft => Message::PaneFocusAdjacent(pane_grid::Direction::Left),
Action::PaneFocusUp => Message::PaneFocusAdjacent(pane_grid::Direction::Up), Self::PaneFocusRight => Message::PaneFocusAdjacent(pane_grid::Direction::Right),
Action::PaneSplitHorizontal => Message::PaneSplit(pane_grid::Axis::Horizontal), Self::PaneFocusUp => Message::PaneFocusAdjacent(pane_grid::Direction::Up),
Action::PaneSplitVertical => Message::PaneSplit(pane_grid::Axis::Vertical), Self::PaneSplitHorizontal => Message::PaneSplit(pane_grid::Axis::Horizontal),
Action::PaneToggleMaximized => Message::PaneToggleMaximized, Self::PaneSplitVertical => Message::PaneSplit(pane_grid::Axis::Vertical),
Action::Paste => Message::Paste(entity_opt), Self::PaneToggleMaximized => Message::PaneToggleMaximized,
Action::ProfileOpen(profile_id) => Message::ProfileOpen(profile_id), Self::Paste => Message::Paste(entity_opt),
Action::Profiles => Message::ToggleContextPage(ContextPage::Profiles), Self::PastePrimary => Message::PastePrimary(entity_opt),
Action::SelectAll => Message::SelectAll(entity_opt), Self::ProfileOpen(profile_id) => Message::ProfileOpen(*profile_id),
Action::Settings => Message::ToggleContextPage(ContextPage::Settings), Self::Profiles => Message::ToggleContextPage(ContextPage::Profiles),
Action::ShowHeaderBar(show_headerbar) => Message::ShowHeaderBar(show_headerbar), Self::SelectAll => Message::SelectAll(entity_opt),
Action::TabActivate0 => Message::TabActivateJump(0), Self::Settings => Message::ToggleContextPage(ContextPage::Settings),
Action::TabActivate1 => Message::TabActivateJump(1), Self::ShowHeaderBar(show_headerbar) => Message::ShowHeaderBar(*show_headerbar),
Action::TabActivate2 => Message::TabActivateJump(2), Self::TabActivate0 => Message::TabActivateJump(0),
Action::TabActivate3 => Message::TabActivateJump(3), Self::TabActivate1 => Message::TabActivateJump(1),
Action::TabActivate4 => Message::TabActivateJump(4), Self::TabActivate2 => Message::TabActivateJump(2),
Action::TabActivate5 => Message::TabActivateJump(5), Self::TabActivate3 => Message::TabActivateJump(3),
Action::TabActivate6 => Message::TabActivateJump(6), Self::TabActivate4 => Message::TabActivateJump(4),
Action::TabActivate7 => Message::TabActivateJump(7), Self::TabActivate5 => Message::TabActivateJump(5),
Action::TabActivate8 => Message::TabActivateJump(8), Self::TabActivate6 => Message::TabActivateJump(6),
Action::TabClose => Message::TabClose(entity_opt), Self::TabActivate7 => Message::TabActivateJump(7),
Action::TabNew => Message::TabNew, Self::TabActivate8 => Message::TabActivateJump(8),
Action::TabNext => Message::TabNext, Self::TabClose => Message::TabClose(entity_opt),
Action::TabPrev => Message::TabPrev, Self::TabNew => Message::TabNew,
Action::WindowClose => Message::WindowClose, Self::TabNext => Message::TabNext,
Action::WindowNew => Message::WindowNew, Self::TabPrev => Message::TabPrev,
Action::ZoomIn => Message::ZoomIn, Self::WindowClose => Message::WindowClose,
Action::ZoomOut => Message::ZoomOut, Self::WindowNew => Message::WindowNew,
Action::ZoomReset => Message::ZoomReset, Self::ZoomIn => Message::ZoomIn,
Self::ZoomOut => Message::ZoomOut,
Self::ZoomReset => Message::ZoomReset,
} }
} }
} }
@ -272,6 +274,7 @@ pub enum Message {
ColorSchemeTabActivate(widget::segmented_button::Entity), ColorSchemeTabActivate(widget::segmented_button::Entity),
Config(Config), Config(Config),
Copy(Option<segmented_button::Entity>), Copy(Option<segmented_button::Entity>),
CopyPrimary(Option<segmented_button::Entity>),
DefaultBoldFontWeight(usize), DefaultBoldFontWeight(usize),
DefaultDimFontWeight(usize), DefaultDimFontWeight(usize),
DefaultFont(usize), DefaultFont(usize),
@ -284,6 +287,7 @@ pub enum Message {
FindNext, FindNext,
FindPrevious, FindPrevious,
FindSearchValueChanged(String), FindSearchValueChanged(String),
MiddleClick(pane_grid::Pane, Option<segmented_button::Entity>),
FocusFollowMouse(bool), FocusFollowMouse(bool),
Key(Modifiers, Key), Key(Modifiers, Key),
LaunchUrl(String), LaunchUrl(String),
@ -297,10 +301,13 @@ pub enum Message {
PaneSplit(pane_grid::Axis), PaneSplit(pane_grid::Axis),
PaneToggleMaximized, PaneToggleMaximized,
Paste(Option<segmented_button::Entity>), Paste(Option<segmented_button::Entity>),
PastePrimary(Option<segmented_button::Entity>),
PasteValue(Option<segmented_button::Entity>, String), PasteValue(Option<segmented_button::Entity>, String),
ProfileCollapse(ProfileId), ProfileCollapse(ProfileId),
ProfileCommand(ProfileId, String), ProfileCommand(ProfileId, String),
ProfileDirectory(ProfileId, String),
ProfileExpand(ProfileId), ProfileExpand(ProfileId),
ProfileHold(ProfileId, bool),
ProfileName(ProfileId, String), ProfileName(ProfileId, String),
ProfileNew, ProfileNew,
ProfileOpen(ProfileId), ProfileOpen(ProfileId),
@ -459,9 +466,9 @@ impl App {
{ {
let color = Color::from(theme.cosmic().background.base); let color = Color::from(theme.cosmic().background.base);
let bytes = color.into_rgba8(); let bytes = color.into_rgba8();
let data = (bytes[2] as u32) let data = u32::from(bytes[2])
| ((bytes[1] as u32) << 8) | (u32::from(bytes[1]) << 8)
| ((bytes[0] as u32) << 16) | (u32::from(bytes[0]) << 16)
| 0xFF000000; | 0xFF000000;
terminal::WINDOW_BG_COLOR.store(data, Ordering::SeqCst); terminal::WINDOW_BG_COLOR.store(data, Ordering::SeqCst);
} }
@ -498,17 +505,14 @@ impl App {
fn save_color_schemes(&mut self, color_scheme_kind: ColorSchemeKind) -> Command<Message> { fn save_color_schemes(&mut self, color_scheme_kind: ColorSchemeKind) -> Command<Message> {
// Optimized for just saving color_schemes // Optimized for just saving color_schemes
if let Some(ref config_handler) = self.config_handler { if let Some(ref config_handler) = self.config_handler {
match config_handler.set( if let Err(err) = config_handler.set(
match color_scheme_kind { match color_scheme_kind {
ColorSchemeKind::Dark => "color_schemes_dark", ColorSchemeKind::Dark => "color_schemes_dark",
ColorSchemeKind::Light => "color_schemes_light", ColorSchemeKind::Light => "color_schemes_light",
}, },
&self.config.color_schemes(color_scheme_kind), self.config.color_schemes(color_scheme_kind),
) { ) {
Ok(()) => {} log::error!("failed to save config: {}", err);
Err(err) => {
log::error!("failed to save config: {}", err);
}
} }
} }
self.update_color_schemes(); self.update_color_schemes();
@ -663,7 +667,7 @@ impl App {
hash = short_hash.as_str(), hash = short_hash.as_str(),
date = date date = date
)) ))
.on_press(Message::LaunchUrl(format!("{}/commits/{}", repository, hash))) .on_press(Message::LaunchUrl(format!("{repository}/commits/{hash}")))
.padding(0) .padding(0)
.into(), .into(),
]) ])
@ -753,7 +757,7 @@ impl App {
.into(), .into(),
); );
for error in self.color_scheme_errors.iter() { for error in &self.color_scheme_errors {
sections.push( sections.push(
widget::row::with_children(vec![ widget::row::with_children(vec![
icon_cache_get("dialog-error-symbolic", 16) icon_cache_get("dialog-error-symbolic", 16)
@ -796,9 +800,8 @@ impl App {
if !self.config.profiles.is_empty() { if !self.config.profiles.is_empty() {
let mut profiles_section = widget::settings::view_section(""); let mut profiles_section = widget::settings::view_section("");
for (profile_name, profile_id) in self.config.profile_names() { for (profile_name, profile_id) in self.config.profile_names() {
let profile = match self.config.profiles.get(&profile_id) { let Some(profile) = self.config.profiles.get(&profile_id) else {
Some(some) => some, continue;
None => continue,
}; };
let expanded = self.profile_expanded == Some(profile_id); let expanded = self.profile_expanded == Some(profile_id);
@ -858,6 +861,16 @@ impl App {
]) ])
.spacing(space_xxxs) .spacing(space_xxxs)
.into(), .into(),
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)
})
.into(),
])
.spacing(space_xxxs)
.into(),
widget::column::with_children(vec![ widget::column::with_children(vec![
widget::text(fl!("tab-title")).into(), widget::text(fl!("tab-title")).into(),
widget::text_input("", &profile.tab_title) widget::text_input("", &profile.tab_title)
@ -908,18 +921,35 @@ impl App {
.add( .add(
widget::settings::item::builder(fl!("make-default")).control( widget::settings::item::builder(fl!("make-default")).control(
widget::toggler( widget::toggler(
"".to_string(), None,
self.get_default_profile().is_some_and(|p| p == profile_id), self.get_default_profile().is_some_and(|p| p == profile_id),
move |t| Message::UpdateDefaultProfile((t, profile_id)), move |t| Message::UpdateDefaultProfile((t, profile_id)),
), ),
), ),
)
.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(),
widget::horizontal_space(Length::Fill).into(),
widget::toggler(None, profile.hold, move |t| {
Message::ProfileHold(profile_id, t)
})
.into(),
])
.align_items(Alignment::Center)
.padding([0, space_s]),
); );
let padding = Padding { let padding = Padding {
top: 0.0, top: 0.0,
bottom: 0.0, bottom: 0.0,
left: space_s as f32, left: space_s.into(),
right: space_s as f32, right: space_s.into(),
}; };
profiles_section = profiles_section =
profiles_section.add(widget::container(expanded_section).padding(padding)) profiles_section.add(widget::container(expanded_section).padding(padding))
@ -1144,7 +1174,19 @@ impl App {
self.pane_model.focus = pane; self.pane_model.focus = pane;
match &self.term_event_tx_opt { match &self.term_event_tx_opt {
Some(term_event_tx) => { Some(term_event_tx) => {
match self.themes.get(&self.config.syntax_theme(profile_id_opt)) { let colors = self
.themes
.get(&self.config.syntax_theme(profile_id_opt))
.or_else(|| match self.config.color_scheme_kind() {
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,
)),
});
match colors {
Some(colors) => { Some(colors) => {
let current_pane = self.pane_model.focus; let current_pane = self.pane_model.focus;
if let Some(tab_model) = self.pane_model.active_mut() { if let Some(tab_model) = self.pane_model.active_mut() {
@ -1153,7 +1195,6 @@ impl App {
.and_then(|profile_id| self.config.profiles.get(&profile_id)) .and_then(|profile_id| self.config.profiles.get(&profile_id))
{ {
Some(profile) => { Some(profile) => {
if !profile.tab_title.is_empty() {}
let mut shell = None; let mut shell = None;
if let Some(mut args) = shlex::split(&profile.command) { if let Some(mut args) = shlex::split(&profile.command) {
if !args.is_empty() { if !args.is_empty() {
@ -1161,17 +1202,19 @@ impl App {
shell = Some(tty::Shell::new(command, args)); shell = Some(tty::Shell::new(command, args));
} }
} }
let working_directory = (!profile.working_directory.is_empty())
.then(|| profile.working_directory.clone().into());
let options = tty::Options { let options = tty::Options {
shell, shell,
//TODO: configurable working directory? working_directory,
working_directory: None, hold: profile.hold,
//TODO: configurable hold (keep open when child exits)? env: HashMap::new(),
hold: false,
}; };
let tab_title_override = if !profile.tab_title.is_empty() { let tab_title_override = if profile.tab_title.is_empty() {
Some(profile.tab_title.clone())
} else {
None None
} else {
Some(profile.tab_title.clone())
}; };
(options, tab_title_override) (options, tab_title_override)
} }
@ -1226,7 +1269,7 @@ impl App {
log::warn!("tried to create new tab before having event channel"); log::warn!("tried to create new tab before having event channel");
} }
} }
return self.update_title(Some(pane)); self.update_title(Some(pane))
} }
} }
@ -1307,7 +1350,7 @@ impl Application for App {
let mut font_size_names = Vec::new(); let mut font_size_names = Vec::new();
let mut font_sizes = Vec::new(); let mut font_sizes = Vec::new();
for font_size in 4..=32 { for font_size in 4..=32 {
font_size_names.push(format!("{}px", font_size)); font_size_names.push(format!("{font_size}px"));
font_sizes.push(font_size); font_sizes.push(font_size);
} }
@ -1358,7 +1401,7 @@ impl Application for App {
let mut terminal_ids = HashMap::new(); let mut terminal_ids = HashMap::new();
terminal_ids.insert(pane_model.focus, widget::Id::unique()); terminal_ids.insert(pane_model.focus, widget::Id::unique());
let mut app = App { let mut app = Self {
core, core,
pane_model, pane_model,
config_handler: flags.config_handler, config_handler: flags.config_handler,
@ -1422,10 +1465,10 @@ impl Application for App {
} }
fn on_context_drawer(&mut self) -> Command<Message> { fn on_context_drawer(&mut self) -> Command<Message> {
if !self.core.window.show_context { if self.core.window.show_context {
self.update_focus()
} else {
Command::none() Command::none()
} else {
self.update_focus()
} }
} }
@ -1436,15 +1479,10 @@ impl Application for App {
($name: ident, $value: expr) => { ($name: ident, $value: expr) => {
match &self.config_handler { match &self.config_handler {
Some(config_handler) => { Some(config_handler) => {
match paste::paste! { self.config.[<set_ $name>](config_handler, $value) } { if let Err(err) =
Ok(_) => {} paste::paste! { self.config.[<set_ $name>](config_handler, $value) }
Err(err) => { {
log::warn!( log::warn!("failed to save config {:?}: {}", stringify!($name), err);
"failed to save config {:?}: {}",
stringify!($name),
err
);
}
} }
} }
None => { None => {
@ -1490,7 +1528,7 @@ impl Application for App {
move |result| { move |result| {
Message::ColorSchemeExportResult( Message::ColorSchemeExportResult(
color_scheme_kind, color_scheme_kind,
color_scheme_id.clone(), color_scheme_id,
result, result,
) )
}, },
@ -1514,9 +1552,8 @@ impl Application for App {
&color_scheme, &color_scheme,
ron::ser::PrettyConfig::new(), ron::ser::PrettyConfig::new(),
) { ) {
Ok(ron) => match fs::write(path, &ron) { Ok(ron) => {
Ok(()) => {} if let Err(err) = fs::write(path, ron) {
Err(err) => {
log::error!( log::error!(
"failed to export {:?} to {:?}: {}", "failed to export {:?} to {:?}: {}",
color_scheme_id, color_scheme_id,
@ -1524,7 +1561,7 @@ impl Application for App {
err err
); );
} }
}, }
Err(err) => { Err(err) => {
log::error!( log::error!(
"failed to serialize color scheme {:?}: {}", "failed to serialize color scheme {:?}: {}",
@ -1558,12 +1595,12 @@ impl Application for App {
self.dialog_opt = None; self.dialog_opt = None;
if let DialogResult::Open(paths) = result { if let DialogResult::Open(paths) = result {
self.color_scheme_errors.clear(); self.color_scheme_errors.clear();
for path in paths.iter() { for path in &paths {
let mut file = match fs::File::open(path) { let mut file = match fs::File::open(path) {
Ok(ok) => ok, Ok(ok) => ok,
Err(err) => { Err(err) => {
self.color_scheme_errors self.color_scheme_errors
.push(format!("Failed to open {:?}: {}", path, err)); .push(format!("Failed to open {path:?}: {err}"));
continue; continue;
} }
}; };
@ -1582,7 +1619,7 @@ impl Application for App {
} }
Err(err) => { Err(err) => {
self.color_scheme_errors self.color_scheme_errors
.push(format!("Failed to parse {:?}: {}", path, err)); .push(format!("Failed to parse {path:?}: {err}"));
} }
} }
} }
@ -1645,6 +1682,23 @@ impl Application for App {
} }
return self.update_focus(); return self.update_focus();
} }
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() {
return Command::batch([
clipboard::write_primary(text),
self.update_focus(),
]);
}
}
} else {
log::warn!("Failed to get focused pane");
}
}
Message::DefaultFont(index) => { Message::DefaultFont(index) => {
match self.font_names.get(index) { match self.font_names.get(index) {
Some(font_name) => { Some(font_name) => {
@ -1793,22 +1847,31 @@ impl Application for App {
Message::FindSearchValueChanged(value) => { Message::FindSearchValueChanged(value) => {
self.find_search_value = value; self.find_search_value = value;
} }
Message::MiddleClick(pane, entity_opt) => {
self.pane_model.focus = pane;
return Command::batch([
self.update_focus(),
clipboard::read_primary(move |value_opt| match value_opt {
Some(value) => message::app(Message::PasteValue(entity_opt, value)),
None => message::none(),
}),
]);
}
Message::FocusFollowMouse(focus_follow_mouse) => { Message::FocusFollowMouse(focus_follow_mouse) => {
config_set!(focus_follow_mouse, focus_follow_mouse); config_set!(focus_follow_mouse, focus_follow_mouse);
} }
Message::Key(modifiers, key) => { Message::Key(modifiers, key) => {
for (key_bind, action) in self.key_binds.iter() { for (key_bind, action) in &self.key_binds {
if key_bind.matches(modifiers, &key) { if key_bind.matches(modifiers, &key) {
return self.update(action.message(None)); return self.update(action.message(None));
} }
} }
} }
Message::LaunchUrl(url) => match open::that_detached(&url) { Message::LaunchUrl(url) => {
Ok(()) => {} if let Err(err) = open::that_detached(&url) {
Err(err) => {
log::warn!("failed to open {:?}: {}", url, err); log::warn!("failed to open {:?}: {}", url, err);
} }
}, }
Message::Modifiers(modifiers) => { Message::Modifiers(modifiers) => {
self.modifiers = modifiers; self.modifiers = modifiers;
} }
@ -1868,6 +1931,12 @@ impl Application for App {
None => message::none(), None => message::none(),
}); });
} }
Message::PastePrimary(entity_opt) => {
return clipboard::read_primary(move |value_opt| match value_opt {
Some(value) => message::app(Message::PasteValue(entity_opt, value)),
None => message::none(),
});
}
Message::PasteValue(entity_opt, value) => { Message::PasteValue(entity_opt, value) => {
if let Some(tab_model) = self.pane_model.active() { if let Some(tab_model) = self.pane_model.active() {
let entity = entity_opt.unwrap_or_else(|| tab_model.active()); let entity = entity_opt.unwrap_or_else(|| tab_model.active());
@ -1887,9 +1956,21 @@ impl Application for App {
return self.save_profiles(); return self.save_profiles();
} }
} }
Message::ProfileDirectory(profile_id, text) => {
if let Some(profile) = self.config.profiles.get_mut(&profile_id) {
profile.working_directory = text;
return self.save_profiles();
}
}
Message::ProfileExpand(profile_id) => { Message::ProfileExpand(profile_id) => {
self.profile_expanded = Some(profile_id); self.profile_expanded = Some(profile_id);
} }
Message::ProfileHold(profile_id, hold) => {
if let Some(profile) = self.config.profiles.get_mut(&profile_id) {
profile.hold = hold;
return self.save_profiles();
}
}
Message::ProfileName(profile_id, text) => { Message::ProfileName(profile_id, text) => {
if let Some(profile) = self.config.profiles.get_mut(&profile_id) { if let Some(profile) = self.config.profiles.get_mut(&profile_id) {
profile.name = text; profile.name = text;
@ -2237,6 +2318,9 @@ impl Application for App {
} }
} }
} }
TermEvent::ChildExit(_error_code) => {
//Ignore this for now
}
} }
} }
Message::TermEventTx(term_event_tx) => { Message::TermEventTx(term_event_tx) => {
@ -2277,31 +2361,28 @@ impl Application for App {
} }
// Extra work to do to prepare context pages // Extra work to do to prepare context pages
match self.context_page { if let ContextPage::ColorSchemes(color_scheme_kind) = self.context_page {
ContextPage::ColorSchemes(color_scheme_kind) => { self.color_scheme_errors.clear();
self.color_scheme_errors.clear(); self.color_scheme_expanded = None;
self.color_scheme_expanded = None; self.color_scheme_renaming = None;
self.color_scheme_renaming = None; self.color_scheme_tab_model = widget::segmented_button::Model::default();
self.color_scheme_tab_model = widget::segmented_button::Model::default(); let dark_entity = self
let dark_entity = self .color_scheme_tab_model
.color_scheme_tab_model .insert()
.insert() .text(fl!("dark"))
.text(fl!("dark")) .data(ColorSchemeKind::Dark)
.data(ColorSchemeKind::Dark) .id();
.id(); let light_entity = self
let light_entity = self .color_scheme_tab_model
.color_scheme_tab_model .insert()
.insert() .text(fl!("light"))
.text(fl!("light")) .data(ColorSchemeKind::Light)
.data(ColorSchemeKind::Light) .id();
.id(); self.color_scheme_tab_model
self.color_scheme_tab_model .activate(match color_scheme_kind {
.activate(match color_scheme_kind { ColorSchemeKind::Dark => dark_entity,
ColorSchemeKind::Dark => dark_entity, ColorSchemeKind::Light => light_entity,
ColorSchemeKind::Light => light_entity, });
});
}
_ => {}
} }
self.set_context_title(context_page.title()); self.set_context_title(context_page.title());
@ -2395,43 +2476,39 @@ impl Application for App {
} }
let entity = tab_model.active(); let entity = tab_model.active();
let entity_middle_click = tab_model.active();
let terminal_id = self let terminal_id = self
.terminal_ids .terminal_ids
.get(&pane) .get(&pane)
.cloned() .cloned()
.unwrap_or_else(widget::Id::unique); .unwrap_or_else(widget::Id::unique);
match tab_model.data::<Mutex<Terminal>>(entity) { if let Some(terminal) = tab_model.data::<Mutex<Terminal>>(entity) {
Some(terminal) => { let mut terminal_box = terminal_box(terminal)
let mut terminal_box = terminal_box(terminal) .id(terminal_id)
.id(terminal_id) .on_context_menu(move |position_opt| {
.on_context_menu(move |position_opt| { Message::TabContextMenu(pane, position_opt)
Message::TabContextMenu(pane, position_opt) })
}) .on_middle_click(move || Message::MiddleClick(pane, Some(entity_middle_click)))
.opacity(self.config.opacity_ratio()) .opacity(self.config.opacity_ratio())
.padding(space_xxs); .padding(space_xxs);
if self.config.focus_follow_mouse { if self.config.focus_follow_mouse {
terminal_box = terminal_box = terminal_box.on_mouse_enter(move || Message::MouseEnter(pane));
terminal_box.on_mouse_enter(move || Message::MouseEnter(pane));
}
let context_menu = {
let terminal = terminal.lock().unwrap();
terminal.context_menu
};
let tab_element: Element<'_, Message> = match context_menu {
Some(point) => widget::popover(terminal_box.context_menu(point))
.popup(menu::context_menu(&self.config, &self.key_binds, entity))
.position(widget::popover::Position::Point(point))
.into(),
None => terminal_box.into(),
};
tab_column = tab_column.push(tab_element);
}
None => {
//TODO
} }
let context_menu = {
let terminal = terminal.lock().unwrap();
terminal.context_menu
};
let tab_element: Element<'_, Message> = match context_menu {
Some(point) => widget::popover(terminal_box.context_menu(point))
.popup(menu::context_menu(&self.config, &self.key_binds, entity))
.position(widget::popover::Position::Point(point))
.into(),
None => terminal_box.into(),
};
tab_column = tab_column.push(tab_element);
} }
//Only draw find in the currently focused pane //Only draw find in the currently focused pane
@ -2487,10 +2564,10 @@ impl Application for App {
.padding(space_xxs) .padding(space_xxs)
.spacing(space_xxs); .spacing(space_xxs);
tab_column = tab_column.push( tab_column = tab_column
widget::cosmic_container::container(find_widget) .push(widget::layer_container(find_widget).layer(cosmic_theme::Layer::Primary));
.layer(cosmic_theme::Layer::Primary), } else {
); // TODO
} }
pane_grid::Content::new(tab_column) pane_grid::Content::new(tab_column)
@ -2519,6 +2596,9 @@ impl Application for App {
Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => { Event::Keyboard(KeyEvent::ModifiersChanged(modifiers)) => {
Some(Message::Modifiers(modifiers)) Some(Message::Modifiers(modifiers))
} }
Event::Mouse(MouseEvent::ButtonReleased(MouseButton::Left)) => {
Some(Message::CopyPrimary(None))
}
_ => None, _ => None,
}), }),
subscription::channel( subscription::channel(

View file

@ -1,38 +1,24 @@
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-only
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::{items as menu_items, root as menu_root, Item as MenuItem};
use cosmic::{ use cosmic::{
//TODO: export in cosmic::widget
iced::{ iced::{
widget::{column, horizontal_rule, horizontal_space}, widget::{column, horizontal_rule, horizontal_space},
Alignment, Background, Length, Alignment, Background, Length,
}, },
iced_core::Border, iced_core::Border,
theme, menu_button, theme,
widget::{ widget::{
self, self,
menu::{ItemHeight, ItemWidth, MenuBar, MenuTree}, menu::{ItemHeight, ItemWidth, MenuBar, Tree as MenuTree},
segmented_button, segmented_button,
}, },
Element, Element,
}; };
use std::collections::HashMap; use std::collections::HashMap;
use crate::{fl, Action, ColorSchemeId, ColorSchemeKind, Config, KeyBind, Message}; use crate::{fl, Action, ColorSchemeId, ColorSchemeKind, Config, Message};
macro_rules! menu_button {
($($x:expr),+ $(,)?) => (
widget::button(
widget::Row::with_children(
vec![$(Element::from($x)),+]
)
.align_items(Alignment::Center)
)
.height(Length::Fixed(32.0))
.padding([4, 16])
.width(Length::Fill)
.style(theme::Button::MenuItem)
);
}
pub fn context_menu<'a>( pub fn context_menu<'a>(
config: &Config, config: &Config,
@ -40,7 +26,7 @@ pub fn context_menu<'a>(
entity: segmented_button::Entity, entity: segmented_button::Entity,
) -> Element<'a, Message> { ) -> Element<'a, Message> {
let find_key = |action: &Action| -> String { let find_key = |action: &Action| -> String {
for (key_bind, key_action) in key_binds.iter() { for (key_bind, key_action) in key_binds {
if action == key_action { if action == key_action {
return key_bind.to_string(); return key_bind.to_string();
} }
@ -145,90 +131,69 @@ pub fn color_scheme_menu<'a>(
} }
pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message> { pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message> {
//TODO: port to libcosmic
let menu_root = |label| {
widget::button(widget::text(label))
.padding([4, 12])
.style(theme::Button::MenuRoot)
};
let menu_folder =
|label| menu_button!(widget::text(label), horizontal_space(Length::Fill), ">");
let find_key = |action: &Action| -> String {
for (key_bind, key_action) in key_binds.iter() {
if action == key_action {
return key_bind.to_string();
}
}
String::new()
};
let menu_item = |label, action| {
let key = find_key(&action);
MenuTree::new(
menu_button!(
widget::text(label),
horizontal_space(Length::Fill),
widget::text(key)
)
.on_press(action.message(None)),
)
};
let mut profile_items = Vec::with_capacity(config.profiles.len()); let mut profile_items = Vec::with_capacity(config.profiles.len());
for (name, id) in config.profile_names() { for (name, id) in config.profile_names() {
profile_items.push(menu_item(name, Action::ProfileOpen(id))); profile_items.push(MenuItem::Button(name, Action::ProfileOpen(id)));
} }
//TODO: what to do if there are no profiles? //TODO: what to do if there are no profiles?
MenuBar::new(vec![ MenuBar::new(vec![
MenuTree::with_children( MenuTree::with_children(
menu_root(fl!("file")), menu_root(fl!("file")),
vec![ menu_items(
menu_item(fl!("new-tab"), Action::TabNew), key_binds,
menu_item(fl!("new-window"), Action::WindowNew), vec![
MenuTree::new(horizontal_rule(1)), MenuItem::Button(fl!("new-tab"), Action::TabNew),
MenuTree::with_children(menu_folder(fl!("profile")), profile_items), MenuItem::Button(fl!("new-window"), Action::WindowNew),
menu_item(fl!("menu-profiles"), Action::Profiles), MenuItem::Divider,
MenuTree::new(horizontal_rule(1)), MenuItem::Folder(fl!("profile"), profile_items),
menu_item(fl!("close-tab"), Action::TabClose), MenuItem::Button(fl!("menu-profiles"), Action::Profiles),
MenuTree::new(horizontal_rule(1)), MenuItem::Divider,
menu_item(fl!("quit"), Action::WindowClose), MenuItem::Button(fl!("close-tab"), Action::TabClose),
], MenuItem::Divider,
MenuItem::Button(fl!("quit"), Action::WindowClose),
],
),
), ),
MenuTree::with_children( MenuTree::with_children(
menu_root(fl!("edit")), menu_root(fl!("edit")),
vec![ menu_items(
menu_item(fl!("copy"), Action::Copy), key_binds,
menu_item(fl!("paste"), Action::Paste), vec![
menu_item(fl!("select-all"), Action::SelectAll), MenuItem::Button(fl!("copy"), Action::Copy),
MenuTree::new(horizontal_rule(1)), MenuItem::Button(fl!("paste"), Action::Paste),
menu_item(fl!("find"), Action::Find), MenuItem::Button(fl!("select-all"), Action::SelectAll),
], MenuItem::Divider,
MenuItem::Button(fl!("find"), Action::Find),
],
),
), ),
MenuTree::with_children( MenuTree::with_children(
menu_root(fl!("view")), menu_root(fl!("view")),
vec![ menu_items(
menu_item(fl!("zoom-in"), Action::ZoomIn), key_binds,
menu_item(fl!("zoom-reset"), Action::ZoomReset), vec![
menu_item(fl!("zoom-out"), Action::ZoomOut), MenuItem::Button(fl!("zoom-in"), Action::ZoomIn),
MenuTree::new(horizontal_rule(1)), MenuItem::Button(fl!("zoom-reset"), Action::ZoomReset),
menu_item(fl!("next-tab"), Action::TabNext), MenuItem::Button(fl!("zoom-out"), Action::ZoomOut),
menu_item(fl!("previous-tab"), Action::TabPrev), MenuItem::Divider,
MenuTree::new(horizontal_rule(1)), MenuItem::Button(fl!("next-tab"), Action::TabNext),
menu_item(fl!("split-horizontal"), Action::PaneSplitHorizontal), MenuItem::Button(fl!("previous-tab"), Action::TabPrev),
menu_item(fl!("split-vertical"), Action::PaneSplitVertical), MenuItem::Divider,
menu_item(fl!("pane-toggle-maximize"), Action::PaneToggleMaximized), MenuItem::Button(fl!("split-horizontal"), Action::PaneSplitHorizontal),
MenuTree::new(horizontal_rule(1)), MenuItem::Button(fl!("split-vertical"), Action::PaneSplitVertical),
menu_item( MenuItem::Button(fl!("pane-toggle-maximize"), Action::PaneToggleMaximized),
fl!("menu-color-schemes"), MenuItem::Divider,
Action::ColorSchemes(config.color_scheme_kind()), MenuItem::Button(
), fl!("menu-color-schemes"),
menu_item(fl!("menu-settings"), Action::Settings), Action::ColorSchemes(config.color_scheme_kind()),
MenuTree::new(horizontal_rule(1)), ),
menu_item(fl!("menu-about"), Action::About), MenuItem::Button(fl!("menu-settings"), Action::Settings),
], MenuItem::Divider,
MenuItem::Button(fl!("menu-about"), Action::About),
],
),
), ),
]) ])
.item_height(ItemHeight::Dynamic(40)) .item_height(ItemHeight::Dynamic(40))

View file

@ -15,7 +15,7 @@ pub struct MouseReporter {
} }
impl MouseReporter { impl MouseReporter {
fn button_number(&self, button: Button) -> Option<u8> { fn button_number(button: Button) -> Option<u8> {
match button { match button {
Button::Left => Some(0), Button::Left => Some(0),
Button::Middle => Some(1), Button::Middle => Some(1),
@ -36,10 +36,10 @@ impl MouseReporter {
) -> Option<Vec<u8>> { ) -> Option<Vec<u8>> {
//Buttons are handle slightly different between normal and sgr //Buttons are handle slightly different between normal and sgr
//for normal/utf8 the button release is always reported as button 3 //for normal/utf8 the button release is always reported as button 3
let Some(mut button) = (match event { let mut button = (match event {
Event::Mouse(MouseEvent::ButtonPressed(b)) => { Event::Mouse(MouseEvent::ButtonPressed(b)) => {
self.button = Some(b); self.button = Some(b);
self.button_number(b) Self::button_number(b)
} }
Event::Mouse(MouseEvent::ButtonReleased(_b)) => { Event::Mouse(MouseEvent::ButtonReleased(_b)) => {
self.button = None; self.button = None;
@ -59,14 +59,10 @@ impl MouseReporter {
//character, Cb). //character, Cb).
//For example, motion into cell x,y with button 1 down is reported as //For example, motion into cell x,y with button 1 down is reported as
//CSI M @ CxCy ( @ = 32 + 0 (button 1) + 32 (motion indicator) ). //CSI M @ CxCy ( @ = 32 + 0 (button 1) + 32 (motion indicator) ).
self.button self.button.and_then(Self::button_number).map(|b| b + 32)
.and_then(|button| self.button_number(button))
.map(|b| b + 32)
} }
_ => None, _ => None,
}) else { })?;
return None;
};
if modifiers.shift() { if modifiers.shift() {
button += 4; button += 4;
@ -127,16 +123,16 @@ impl MouseReporter {
x: u32, x: u32,
y: u32, y: u32,
) -> Option<Vec<u8>> { ) -> Option<Vec<u8>> {
let Some((button_no, event_code)) = (match event { let (button_no, event_code) = (match event {
Event::Mouse(MouseEvent::ButtonPressed(button)) => { Event::Mouse(MouseEvent::ButtonPressed(button)) => {
//Button pressed is reported as button 0,1,2 and event code M //Button pressed is reported as button 0,1,2 and event code M
self.button = Some(button); self.button = Some(button);
Some((self.button_number(button), "M")) Some((Self::button_number(button), "M"))
} }
Event::Mouse(MouseEvent::ButtonReleased(button)) => { Event::Mouse(MouseEvent::ButtonReleased(button)) => {
//Button pressed is reported as button 0,1,2 and event code m //Button pressed is reported as button 0,1,2 and event code m
self.button = None; self.button = None;
Some((self.button_number(button), "m")) Some((Self::button_number(button), "m"))
} }
Event::Mouse(MouseEvent::CursorMoved { .. }) => { Event::Mouse(MouseEvent::CursorMoved { .. }) => {
//Button pressed is reported as button 32 + 0,1,2 and event code M //Button pressed is reported as button 32 + 0,1,2 and event code M
@ -148,12 +144,10 @@ impl MouseReporter {
self.last_movment_y = Some(y); self.last_movment_y = Some(y);
} }
self.button self.button
.map(|button| (self.button_number(button).map(|b| b + 32), "M")) .map(|button| (Self::button_number(button).map(|b| b + 32), "M"))
} }
_ => None, _ => None,
}) else { })?;
return None;
};
if let Some(mut button_no) = button_no { if let Some(mut button_no) = button_no {
if modifiers.shift() { if modifiers.shift() {
@ -174,7 +168,6 @@ impl MouseReporter {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn report_sgr_mouse_wheel_scroll( pub fn report_sgr_mouse_wheel_scroll(
&self,
terminal: &Terminal, terminal: &Terminal,
term_cell_width: f32, term_cell_width: f32,
term_cell_height: f32, term_cell_height: f32,
@ -217,7 +210,6 @@ impl MouseReporter {
//Emulate mouse wheel scroll with up/down arrows. Using mouse spec uses //Emulate mouse wheel scroll with up/down arrows. Using mouse spec uses
//scroll-back and scroll-forw actions, which moves whole windows like page up/page down. //scroll-back and scroll-forw actions, which moves whole windows like page up/page down.
pub fn report_mouse_wheel_as_arrows( pub fn report_mouse_wheel_as_arrows(
&self,
terminal: &Terminal, terminal: &Terminal,
term_cell_width: f32, term_cell_width: f32,
term_cell_height: f32, term_cell_height: f32,

View file

@ -21,7 +21,8 @@ use cosmic::{
widget::{pane_grid, segmented_button}, widget::{pane_grid, segmented_button},
}; };
use cosmic_text::{ use cosmic_text::{
Attrs, AttrsList, Buffer, BufferLine, CacheKeyFlags, Family, Metrics, Shaping, Weight, Wrap, Attrs, AttrsList, Buffer, BufferLine, CacheKeyFlags, Family, LineEnding, Metrics, Shaping,
Weight, Wrap,
}; };
use indexmap::IndexSet; use indexmap::IndexSet;
use std::{ use std::{
@ -110,25 +111,25 @@ fn convert_color(colors: &Colors, color: Color) -> cosmic_text::Color {
let rgb = match color { let rgb = match color {
Color::Named(named_color) => match colors[named_color] { Color::Named(named_color) => match colors[named_color] {
Some(rgb) => rgb, Some(rgb) => rgb,
None => match named_color { None => {
NamedColor::Background => { if named_color == NamedColor::Background {
// Allow using an unset background // Allow using an unset background
return cosmic_text::Color(WINDOW_BG_COLOR.load(Ordering::SeqCst)); return cosmic_text::Color(WINDOW_BG_COLOR.load(Ordering::SeqCst));
} } else {
_ => {
log::warn!("missing named color {:?}", named_color); log::warn!("missing named color {:?}", named_color);
Rgb::default() Rgb::default()
} }
}, }
}, },
Color::Spec(rgb) => rgb, Color::Spec(rgb) => rgb,
Color::Indexed(index) => match colors[index as usize] { Color::Indexed(index) => {
Some(rgb) => rgb, if let Some(rgb) = colors[index as usize] {
None => { rgb
} else {
log::warn!("missing indexed color {}", index); log::warn!("missing indexed color {}", index);
Rgb::default() Rgb::default()
} }
}, }
}; };
cosmic_text::Color::rgb(rgb.r, rgb.g, rgb.b) cosmic_text::Color::rgb(rgb.r, rgb.g, rgb.b)
} }
@ -276,7 +277,7 @@ impl Terminal {
let window_id = 0; let window_id = 0;
let pty = tty::new(&options, size.into(), window_id)?; let pty = tty::new(&options, size.into(), window_id)?;
let pty_event_loop = EventLoop::new(term.clone(), event_proxy, pty, options.hold, false); let pty_event_loop = EventLoop::new(term.clone(), event_proxy, pty, options.hold, false)?;
let notifier = Notifier(pty_event_loop.channel()); let notifier = Notifier(pty_event_loop.channel());
let _pty_join_handle = pty_event_loop.spawn(); let _pty_join_handle = pty_event_loop.spawn();
@ -437,9 +438,8 @@ impl Terminal {
} }
} }
let search_regex = match &mut self.search_regex_opt { let Some(search_regex) = &mut self.search_regex_opt else {
Some(some) => some, return;
None => return,
}; };
// Determine search origin // Determine search origin
@ -572,11 +572,14 @@ impl Terminal {
} }
} }
if changed { if changed {
self.update_colors(config);
update = true; update = true;
} }
} }
//TODO: this is done on every set_config because the changed boolean above does not capture
// WINDOW_BG changes
self.update_colors(config);
if update_cell_size { if update_cell_size {
self.update_cell_size(); self.update_cell_size();
} else if update { } else if update {
@ -664,13 +667,18 @@ impl Terminal {
while line_i >= buffer.lines.len() { while line_i >= buffer.lines.len() {
buffer.lines.push(BufferLine::new( buffer.lines.push(BufferLine::new(
"", "",
LineEnding::default(),
AttrsList::new(self.default_attrs), AttrsList::new(self.default_attrs),
Shaping::Advanced, Shaping::Advanced,
)); ));
buffer.set_redraw(true); buffer.set_redraw(true);
} }
if buffer.lines[line_i].set_text(text.clone(), attrs_list.clone()) { if buffer.lines[line_i].set_text(
text.clone(),
LineEnding::default(),
attrs_list.clone(),
) {
buffer.set_redraw(true); buffer.set_redraw(true);
} }
line_i += 1; line_i += 1;
@ -783,13 +791,14 @@ impl Terminal {
while line_i >= buffer.lines.len() { while line_i >= buffer.lines.len() {
buffer.lines.push(BufferLine::new( buffer.lines.push(BufferLine::new(
"", "",
LineEnding::default(),
AttrsList::new(self.default_attrs), AttrsList::new(self.default_attrs),
Shaping::Advanced, Shaping::Advanced,
)); ));
buffer.set_redraw(true); buffer.set_redraw(true);
} }
if buffer.lines[line_i].set_text(text, attrs_list) { if buffer.lines[line_i].set_text(text, LineEnding::default(), attrs_list) {
buffer.set_redraw(true); buffer.set_redraw(true);
} }
line_i += 1; line_i += 1;
@ -853,7 +862,7 @@ impl Terminal {
let term_lock = self.term.lock(); let term_lock = self.term.lock();
let mode = term_lock.mode(); let mode = term_lock.mode();
if mode.contains(TermMode::SGR_MOUSE) { if mode.contains(TermMode::SGR_MOUSE) {
self.mouse_reporter.report_sgr_mouse_wheel_scroll( MouseReporter::report_sgr_mouse_wheel_scroll(
self, self,
self.size().cell_width, self.size().cell_width,
self.size().cell_height, self.size().cell_height,
@ -863,7 +872,7 @@ impl Terminal {
y, y,
); );
} else { } else {
self.mouse_reporter.report_mouse_wheel_as_arrows( MouseReporter::report_mouse_wheel_as_arrows(
self, self,
self.size().cell_width, self.size().cell_width,
self.size().cell_height, self.size().cell_height,
@ -876,6 +885,8 @@ impl Terminal {
impl Drop for Terminal { impl Drop for Terminal {
fn drop(&mut self) { fn drop(&mut self) {
// Ensure shutdown on terminal drop // Ensure shutdown on terminal drop
self.notifier.0.send(Msg::Shutdown); if let Err(err) = self.notifier.0.send(Msg::Shutdown) {
log::warn!("Failed to send shutdown message on dropped terminal: {err}");
}
} }
} }

View file

@ -5,6 +5,7 @@ use alacritty_terminal::{
selection::{Selection, SelectionType}, selection::{Selection, SelectionType},
term::{cell::Flags, TermMode}, term::{cell::Flags, TermMode},
}; };
use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::{ use cosmic::{
cosmic_theme::palette::{blend::Compose, WithAlpha}, cosmic_theme::palette::{blend::Compose, WithAlpha},
iced::{ iced::{
@ -40,11 +41,7 @@ use std::{
time::{Duration, Instant}, time::{Duration, Instant},
}; };
use crate::{ use crate::{key_bind::key_binds, terminal::Metadata, Action, Terminal, TerminalScroll};
key_bind::{key_binds, KeyBind},
terminal::Metadata,
Action, Terminal, TerminalScroll,
};
pub struct TerminalBox<'a, Message> { pub struct TerminalBox<'a, Message> {
terminal: &'a Mutex<Terminal>, terminal: &'a Mutex<Terminal>,
@ -57,6 +54,7 @@ pub struct TerminalBox<'a, Message> {
on_mouse_enter: Option<Box<dyn Fn() -> Message + 'a>>, on_mouse_enter: Option<Box<dyn Fn() -> Message + 'a>>,
opacity: Option<f32>, opacity: Option<f32>,
mouse_inside_boundary: Option<bool>, mouse_inside_boundary: Option<bool>,
on_middle_click: Option<Box<dyn Fn() -> Message + 'a>>,
key_binds: HashMap<KeyBind, Action>, key_binds: HashMap<KeyBind, Action>,
} }
@ -76,6 +74,7 @@ where
on_mouse_enter: None, on_mouse_enter: None,
opacity: None, opacity: None,
mouse_inside_boundary: None, mouse_inside_boundary: None,
on_middle_click: None,
key_binds: key_binds(), key_binds: key_binds(),
} }
} }
@ -118,6 +117,11 @@ where
self self
} }
pub fn on_middle_click(mut self, on_middle_click: impl Fn() -> Message + 'a) -> Self {
self.on_middle_click = Some(Box::new(on_middle_click));
self
}
pub fn opacity(mut self, opacity: f32) -> Self { pub fn opacity(mut self, opacity: f32) -> Self {
self.opacity = Some(opacity); self.opacity = Some(opacity);
self self
@ -168,10 +172,9 @@ where
// Calculate layout lines // Calculate layout lines
terminal.with_buffer(|buffer| { terminal.with_buffer(|buffer| {
let mut layout_lines = 0; let mut layout_lines = 0;
for line in buffer.lines.iter() { for line in &buffer.lines {
match line.layout_opt() { if let Some(layout) = line.layout_opt() {
Some(layout) => layout_lines += layout.len(), layout_lines += layout.len()
None => (),
} }
} }
@ -237,7 +240,7 @@ where
let state = tree.state.downcast_ref::<State>(); let state = tree.state.downcast_ref::<State>();
let cosmic_theme = theme.cosmic(); let cosmic_theme = theme.cosmic();
let scrollbar_w = cosmic_theme.spacing.space_xxs as f32; let scrollbar_w = f32::from(cosmic_theme.spacing.space_xxs);
let view_position = layout.position() + [self.padding.left, self.padding.top].into(); let view_position = layout.position() + [self.padding.left, self.padding.top].into();
let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32) let view_w = cmp::min(viewport.width as i32, layout.bounds().width as i32)
@ -274,12 +277,12 @@ where
..Default::default() ..Default::default()
}, },
Color::new( Color::new(
background_color.r() as f32 / 255.0, f32::from(background_color.r()) / 255.0,
background_color.g() as f32 / 255.0, f32::from(background_color.g()) / 255.0,
background_color.b() as f32 / 255.0, f32::from(background_color.b()) / 255.0,
match self.opacity { match self.opacity {
Some(opacity) => opacity, Some(opacity) => opacity,
None => background_color.a() as f32 / 255.0, None => f32::from(background_color.a()) / 255.0,
}, },
), ),
); );
@ -325,10 +328,10 @@ where
) { ) {
let cosmic_text_to_iced_color = |color: cosmic_text::Color| { let cosmic_text_to_iced_color = |color: cosmic_text::Color| {
Color::new( Color::new(
color.r() as f32 / 255.0, f32::from(color.r()) / 255.0,
color.g() as f32 / 255.0, f32::from(color.g()) / 255.0,
color.b() as f32 / 255.0, f32::from(color.b()) / 255.0,
color.a() as f32 / 255.0, f32::from(color.a()) / 255.0,
) )
}; };
@ -366,8 +369,7 @@ where
} }
if !metadata.flags.is_empty() { if !metadata.flags.is_empty() {
let style_line_height = let style_line_height = (self.glyph_font_size / 10.0).clamp(2.0, 16.0);
(self.glyph_font_size / 10.0).max(2.0).min(16.0);
let line_color = cosmic_text_to_iced_color(metadata.underline_color); let line_color = cosmic_text_to_iced_color(metadata.underline_color);
@ -484,7 +486,7 @@ where
view_position, view_position,
metadata_set, metadata_set,
}; };
for glyph in run.glyphs.iter() { for glyph in run.glyphs {
bg_rect.update(glyph, renderer, state.is_focused); bg_rect.update(glyph, renderer, state.is_focused);
} }
bg_rect.fill(renderer, state.is_focused); bg_rect.fill(renderer, state.is_focused);
@ -602,7 +604,7 @@ where
modifiers, modifiers,
.. ..
}) if state.is_focused => { }) if state.is_focused => {
for (key_bind, _) in self.key_binds.iter() { for key_bind in self.key_binds.keys() {
if key_bind.matches(modifiers, &Key::Named(named)) { if key_bind.matches(modifiers, &Key::Named(named)) {
return Status::Captured; return Status::Captured;
} }
@ -611,8 +613,22 @@ where
let escape_code = match named { let escape_code = match named {
Named::Insert => csi("2", "~", mod_no), Named::Insert => csi("2", "~", mod_no),
Named::Delete => csi("3", "~", mod_no), Named::Delete => csi("3", "~", mod_no),
Named::PageUp => csi("5", "~", mod_no), Named::PageUp => {
Named::PageDown => csi("6", "~", mod_no), if modifiers.shift() {
terminal.scroll(TerminalScroll::PageUp);
None
} else {
csi("5", "~", mod_no)
}
}
Named::PageDown => {
if modifiers.shift() {
terminal.scroll(TerminalScroll::PageDown);
None
} else {
csi("6", "~", mod_no)
}
}
Named::ArrowUp => { Named::ArrowUp => {
if is_app_cursor { if is_app_cursor {
ss3("A", mod_no) ss3("A", mod_no)
@ -642,14 +658,20 @@ where
} }
} }
Named::End => { Named::End => {
if is_app_cursor { if modifiers.shift() {
terminal.scroll(TerminalScroll::Bottom);
None
} else if is_app_cursor {
ss3("F", mod_no) ss3("F", mod_no)
} else { } else {
csi("F", "", mod_no) csi("F", "", mod_no)
} }
} }
Named::Home => { Named::Home => {
if is_app_cursor { if modifiers.shift() {
terminal.scroll(TerminalScroll::Top);
None
} else if is_app_cursor {
ss3("H", mod_no) ss3("H", mod_no)
} else { } else {
csi("H", "", mod_no) csi("H", "", mod_no)
@ -681,8 +703,7 @@ where
match named { match named {
Named::Backspace => { Named::Backspace => {
let code = if modifiers.control() { "\x08" } else { "\x7f" }; let code = if modifiers.control() { "\x08" } else { "\x7f" };
terminal terminal.input_scroll(format!("{alt_prefix}{code}").as_bytes().to_vec());
.input_scroll(format!("{}{}", alt_prefix, code).as_bytes().to_vec());
status = Status::Captured; status = Status::Captured;
} }
Named::Enter => { Named::Enter => {
@ -711,8 +732,7 @@ where
} }
Named::Tab => { Named::Tab => {
let code = if modifiers.shift() { "\x1b[Z" } else { "\x09" }; let code = if modifiers.shift() { "\x1b[Z" } else { "\x09" };
terminal terminal.input_scroll(format!("{alt_prefix}{code}").as_bytes().to_vec());
.input_scroll(format!("{}{}", alt_prefix, code).as_bytes().to_vec());
status = Status::Captured; status = Status::Captured;
} }
_ => {} _ => {}
@ -727,7 +747,7 @@ where
key, key,
.. ..
}) if state.is_focused => { }) if state.is_focused => {
for (key_bind, _) in self.key_binds.iter() { for key_bind in self.key_binds.keys() {
if key_bind.matches(modifiers, &key) { if key_bind.matches(modifiers, &key) {
return Status::Captured; return Status::Captured;
} }
@ -880,6 +900,10 @@ where
} }
} }
} }
} else if button == Button::Middle {
if let Some(on_middle_click) = &self.on_middle_click {
shell.publish(on_middle_click());
}
} }
// Update context menu state // Update context menu state
if let Some(on_context_menu) = &self.on_context_menu { if let Some(on_context_menu) = &self.on_context_menu {
@ -1036,9 +1060,9 @@ fn shade(color: cosmic_text::Color, is_focused: bool) -> cosmic_text::Color {
} else { } else {
let shade = 0.92; let shade = 0.92;
cosmic_text::Color::rgba( cosmic_text::Color::rgba(
(color.r() as f32 * shade) as u8, (f32::from(color.r()) * shade) as u8,
(color.g() as f32 * shade) as u8, (f32::from(color.g()) * shade) as u8,
(color.b() as f32 * shade) as u8, (f32::from(color.b()) * shade) as u8,
color.a(), color.a(),
) )
} }
@ -1078,8 +1102,8 @@ pub struct State {
impl State { impl State {
/// Creates a new [`State`]. /// Creates a new [`State`].
pub fn new() -> State { pub fn new() -> Self {
State { Self {
modifiers: Modifiers::empty(), modifiers: Modifiers::empty(),
click: None, click: None,
dragging: None, dragging: None,
@ -1114,7 +1138,7 @@ meta 0b100000 (32)
caps_lock 0b1000000 (64) caps_lock 0b1000000 (64)
num_lock 0b10000000 (128) num_lock 0b10000000 (128)
*/ */
fn calculate_modifier_number(state: &mut State) -> u8 { fn calculate_modifier_number(state: &State) -> u8 {
let mut mod_no = 0; let mut mod_no = 0;
if state.modifiers.shift() { if state.modifiers.shift() {
mod_no |= 1; mod_no |= 1;
@ -1134,10 +1158,10 @@ fn calculate_modifier_number(state: &mut State) -> u8 {
#[inline(always)] #[inline(always)]
fn csi(code: &str, suffix: &str, modifiers: u8) -> Option<Vec<u8>> { fn csi(code: &str, suffix: &str, modifiers: u8) -> Option<Vec<u8>> {
if modifiers == 1 { if modifiers == 1 {
Some(format!("\x1B[{}{}", code, suffix).as_bytes().to_vec()) Some(format!("\x1B[{code}{suffix}").as_bytes().to_vec())
} else { } else {
Some( Some(
format!("\x1B[{};{}{}", code, modifiers, suffix) format!("\x1B[{code};{modifiers}{suffix}")
.as_bytes() .as_bytes()
.to_vec(), .to_vec(),
) )
@ -1147,8 +1171,8 @@ fn csi(code: &str, suffix: &str, modifiers: u8) -> Option<Vec<u8>> {
#[inline(always)] #[inline(always)]
fn ss3(code: &str, modifiers: u8) -> Option<Vec<u8>> { fn ss3(code: &str, modifiers: u8) -> Option<Vec<u8>> {
if modifiers == 1 { if modifiers == 1 {
Some(format!("\x1B\x4F{}", code).as_bytes().to_vec()) Some(format!("\x1B\x4F{code}").as_bytes().to_vec())
} else { } else {
Some(format!("\x1B[1;{}{}", modifiers, code).as_bytes().to_vec()) Some(format!("\x1B[1;{modifiers}{code}").as_bytes().to_vec())
} }
} }

View file

@ -6,7 +6,9 @@ use hex_color::HexColor;
use palette::{encoding::Srgb, rgb::Rgb as PRgb, FromColor, Okhsl}; use palette::{encoding::Srgb, rgb::Rgb as PRgb, FromColor, Okhsl};
use std::{collections::HashMap, fs}; use std::{collections::HashMap, fs};
use crate::config::{ColorScheme, ColorSchemeAnsi, ColorSchemeKind}; use crate::config::{
ColorScheme, ColorSchemeAnsi, ColorSchemeKind, COSMIC_THEME_DARK, COSMIC_THEME_LIGHT,
};
// Fill missing dim/bright colors with derived values from normal ones. // Fill missing dim/bright colors with derived values from normal ones.
#[allow(dead_code)] #[allow(dead_code)]
@ -53,8 +55,8 @@ impl ColorDerive {
fn color_adj(rgb: Rgb, saturation_adj: f32, lightness_adj: f32) -> Rgb { fn color_adj(rgb: Rgb, saturation_adj: f32, lightness_adj: f32) -> Rgb {
let mut okhsl = Self::rgb_to_okhsl(rgb); let mut okhsl = Self::rgb_to_okhsl(rgb);
okhsl.saturation = (okhsl.saturation + saturation_adj).max(0.0).min(1.0); okhsl.saturation = (okhsl.saturation + saturation_adj).clamp(0.0, 1.0);
okhsl.lightness = (okhsl.lightness + lightness_adj).max(0.0).min(1.0); okhsl.lightness = (okhsl.lightness + lightness_adj).clamp(0.0, 1.0);
Self::okhsl_to_rgb(okhsl) Self::okhsl_to_rgb(okhsl)
} }
@ -338,11 +340,11 @@ fn cosmic_light() -> Colors {
pub fn terminal_themes() -> HashMap<(String, ColorSchemeKind), Colors> { pub fn terminal_themes() -> HashMap<(String, ColorSchemeKind), Colors> {
let mut themes = HashMap::new(); let mut themes = HashMap::new();
themes.insert( themes.insert(
("COSMIC Dark".to_string(), ColorSchemeKind::Dark), (COSMIC_THEME_DARK.to_string(), ColorSchemeKind::Dark),
cosmic_dark(), cosmic_dark(),
); );
themes.insert( themes.insert(
("COSMIC Light".to_string(), ColorSchemeKind::Light), (COSMIC_THEME_LIGHT.to_string(), ColorSchemeKind::Light),
cosmic_light(), cosmic_light(),
); );
themes themes