2023-12-20 13:31:10 -07:00
|
|
|
// Copyright 2023 System76 <info@system76.com>
|
|
|
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
|
|
2023-12-21 10:11:11 -07:00
|
|
|
use alacritty_terminal::{
|
|
|
|
|
config::Config as TermConfig, event::Event as TermEvent, term::color::Colors as TermColors, tty,
|
|
|
|
|
};
|
2023-12-20 13:31:10 -07:00
|
|
|
use cosmic::{
|
2023-12-21 22:13:17 -07:00
|
|
|
app::{message, Command, Core, Settings},
|
2023-12-20 13:31:10 -07:00
|
|
|
cosmic_theme, executor,
|
|
|
|
|
iced::{
|
2023-12-21 22:13:17 -07:00
|
|
|
clipboard, event,
|
2023-12-20 13:31:10 -07:00
|
|
|
futures::SinkExt,
|
2023-12-21 22:13:17 -07:00
|
|
|
keyboard::{Event as KeyEvent, KeyCode, Modifiers},
|
2023-12-20 13:31:10 -07:00
|
|
|
subscription::{self, Subscription},
|
|
|
|
|
widget::row,
|
2023-12-21 22:13:17 -07:00
|
|
|
window, Alignment, Event, Length,
|
2023-12-17 22:51:50 -07:00
|
|
|
},
|
2023-12-20 13:31:10 -07:00
|
|
|
iced_core::Size,
|
|
|
|
|
style,
|
|
|
|
|
widget::{self, segmented_button},
|
|
|
|
|
ApplicationExt, Element,
|
2023-12-17 17:49:39 -07:00
|
|
|
};
|
2023-12-21 09:44:44 -07:00
|
|
|
use std::{any::TypeId, collections::HashMap, sync::Mutex};
|
2023-12-20 13:31:10 -07:00
|
|
|
use tokio::sync::mpsc;
|
2023-12-17 11:53:26 -07:00
|
|
|
|
2023-12-20 14:26:31 -07:00
|
|
|
use self::terminal::{Terminal, TerminalScroll};
|
2023-12-20 13:31:10 -07:00
|
|
|
mod terminal;
|
2023-12-18 13:54:08 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
use self::terminal_box::terminal_box;
|
|
|
|
|
mod terminal_box;
|
2023-12-17 11:53:26 -07:00
|
|
|
|
2023-12-21 09:44:44 -07:00
|
|
|
mod terminal_theme;
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
/// Runs application with these settings
|
|
|
|
|
#[rustfmt::skip]
|
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
|
|
|
env_logger::init();
|
2023-12-17 11:53:26 -07:00
|
|
|
|
2023-12-21 10:11:11 -07:00
|
|
|
// Set up environmental variables for terminal
|
2023-12-21 10:14:57 -07:00
|
|
|
let mut term_config = TermConfig::default();
|
|
|
|
|
// Override TERM for better compatibility
|
|
|
|
|
term_config.env.insert("TERM".to_string(), "xterm-256color".to_string());
|
|
|
|
|
tty::setup_env(&term_config);
|
2023-12-17 11:53:26 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
let settings = Settings::default()
|
|
|
|
|
.antialiasing(true)
|
|
|
|
|
.client_decorations(true)
|
|
|
|
|
.debug(false)
|
2023-12-20 19:54:18 -07:00
|
|
|
.default_icon_theme("Cosmic")
|
2023-12-20 13:31:10 -07:00
|
|
|
.default_text_size(16.0)
|
|
|
|
|
.scale_factor(1.0)
|
|
|
|
|
.size(Size::new(1024., 768.))
|
|
|
|
|
.theme(cosmic::Theme::dark());
|
|
|
|
|
|
2023-12-21 10:14:57 -07:00
|
|
|
cosmic::app::run::<App>(settings, term_config)?;
|
2023-12-20 13:31:10 -07:00
|
|
|
|
|
|
|
|
Ok(())
|
2023-12-17 22:51:50 -07:00
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
/// Messages that are used specifically by our [`App`].
|
|
|
|
|
#[derive(Clone, Debug)]
|
|
|
|
|
pub enum Message {
|
2023-12-21 22:13:17 -07:00
|
|
|
Copy,
|
|
|
|
|
Paste,
|
|
|
|
|
PasteValue(String),
|
2023-12-20 13:31:10 -07:00
|
|
|
TabActivate(segmented_button::Entity),
|
|
|
|
|
TabClose(segmented_button::Entity),
|
|
|
|
|
TabNew,
|
|
|
|
|
TermEvent(segmented_button::Entity, TermEvent),
|
|
|
|
|
TermEventTx(mpsc::Sender<(segmented_button::Entity, TermEvent)>),
|
2023-12-17 11:53:26 -07:00
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
/// The [`App`] stores application-specific state.
|
|
|
|
|
pub struct App {
|
|
|
|
|
core: Core,
|
|
|
|
|
tab_model: segmented_button::Model<segmented_button::SingleSelect>,
|
|
|
|
|
term_event_tx_opt: Option<mpsc::Sender<(segmented_button::Entity, TermEvent)>>,
|
2023-12-21 10:14:57 -07:00
|
|
|
term_config: TermConfig,
|
2023-12-21 09:44:44 -07:00
|
|
|
terminal_theme: String,
|
|
|
|
|
terminal_themes: HashMap<String, TermColors>,
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Implement [`cosmic::Application`] to integrate with COSMIC.
|
|
|
|
|
impl cosmic::Application for App {
|
|
|
|
|
/// Default async executor to use with the app.
|
|
|
|
|
type Executor = executor::Default;
|
|
|
|
|
|
2023-12-21 10:19:16 -07:00
|
|
|
/// Argument received
|
2023-12-21 10:14:57 -07:00
|
|
|
type Flags = TermConfig;
|
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.
|
|
|
|
|
const APP_ID: &'static str = "org.cosmic.AppDemo";
|
|
|
|
|
|
|
|
|
|
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.
|
2023-12-21 11:57:52 -07:00
|
|
|
fn init(mut core: Core, term_config: Self::Flags) -> (Self, Command<Self::Message>) {
|
|
|
|
|
core.window.content_container = false;
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
let mut app = App {
|
|
|
|
|
core,
|
|
|
|
|
tab_model: segmented_button::ModelBuilder::default().build(),
|
|
|
|
|
term_event_tx_opt: None,
|
2023-12-21 10:14:57 -07:00
|
|
|
term_config,
|
2023-12-21 21:22:24 -07:00
|
|
|
terminal_theme: "Cosmic Dark".to_string(),
|
2023-12-21 09:44:44 -07:00
|
|
|
terminal_themes: terminal_theme::terminal_themes(),
|
2023-12-17 22:51:50 -07:00
|
|
|
};
|
2023-12-20 13:31:10 -07:00
|
|
|
|
|
|
|
|
let command = app.update_title();
|
|
|
|
|
|
|
|
|
|
(app, command)
|
2023-12-17 22:51:50 -07:00
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
/// Handle application events here.
|
|
|
|
|
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
|
|
|
|
match message {
|
2023-12-21 22:13:17 -07:00
|
|
|
Message::Copy => {
|
|
|
|
|
if let Some(terminal) = self
|
|
|
|
|
.tab_model
|
|
|
|
|
.data::<Mutex<Terminal>>(self.tab_model.active())
|
|
|
|
|
{
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
let term = terminal.term.lock();
|
|
|
|
|
if let Some(text) = term.selection_to_string() {
|
|
|
|
|
return clipboard::write(text);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Message::Paste => {
|
|
|
|
|
return clipboard::read(|value_opt| match value_opt {
|
|
|
|
|
Some(value) => message::app(Message::PasteValue(value)),
|
|
|
|
|
None => message::none(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
Message::PasteValue(value) => {
|
|
|
|
|
if let Some(terminal) = self
|
|
|
|
|
.tab_model
|
|
|
|
|
.data::<Mutex<Terminal>>(self.tab_model.active())
|
|
|
|
|
{
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.paste(value);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
Message::TabActivate(entity) => {
|
|
|
|
|
self.tab_model.activate(entity);
|
|
|
|
|
return self.update_title();
|
|
|
|
|
}
|
|
|
|
|
Message::TabClose(entity) => {
|
|
|
|
|
// Activate closest item
|
|
|
|
|
if let Some(position) = self.tab_model.position(entity) {
|
|
|
|
|
if position > 0 {
|
|
|
|
|
self.tab_model.activate_position(position - 1);
|
|
|
|
|
} else {
|
|
|
|
|
self.tab_model.activate_position(position + 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-17 22:51:50 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
// Remove item
|
|
|
|
|
self.tab_model.remove(entity);
|
|
|
|
|
|
|
|
|
|
// If that was the last tab, close window
|
|
|
|
|
if self.tab_model.iter().next().is_none() {
|
|
|
|
|
return window::close(window::Id::MAIN);
|
|
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
return self.update_title();
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
Message::TabNew => match &self.term_event_tx_opt {
|
2023-12-21 09:44:44 -07:00
|
|
|
Some(term_event_tx) => match self.terminal_themes.get(&self.terminal_theme) {
|
|
|
|
|
Some(colors) => {
|
|
|
|
|
let entity = self
|
|
|
|
|
.tab_model
|
|
|
|
|
.insert()
|
|
|
|
|
.text("New Terminal")
|
|
|
|
|
.closable()
|
|
|
|
|
.activate()
|
|
|
|
|
.id();
|
2023-12-21 10:14:57 -07:00
|
|
|
let terminal = Terminal::new(
|
|
|
|
|
entity,
|
|
|
|
|
term_event_tx.clone(),
|
|
|
|
|
&self.term_config,
|
|
|
|
|
colors.clone(),
|
|
|
|
|
);
|
2023-12-21 09:44:44 -07:00
|
|
|
self.tab_model
|
|
|
|
|
.data_set::<Mutex<Terminal>>(entity, Mutex::new(terminal));
|
|
|
|
|
}
|
|
|
|
|
None => {
|
|
|
|
|
log::error!("failed to find terminal theme {:?}", self.terminal_theme);
|
|
|
|
|
}
|
|
|
|
|
},
|
2023-12-20 13:31:10 -07:00
|
|
|
None => {
|
|
|
|
|
log::warn!("tried to create new tab before having event channel");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Message::TermEvent(entity, event) => match event {
|
|
|
|
|
TermEvent::Bell => {
|
|
|
|
|
//TODO: audible or visible bell options?
|
|
|
|
|
}
|
|
|
|
|
TermEvent::ColorRequest(index, f) => {
|
|
|
|
|
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
let rgb = terminal.colors()[index].unwrap_or_default();
|
|
|
|
|
let text = f(rgb);
|
2023-12-20 15:01:47 -07:00
|
|
|
terminal.input_no_scroll(text.into_bytes());
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
TermEvent::Exit => {
|
|
|
|
|
return self.update(Message::TabClose(entity));
|
|
|
|
|
}
|
|
|
|
|
TermEvent::PtyWrite(text) => {
|
|
|
|
|
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
2023-12-20 15:01:47 -07:00
|
|
|
terminal.input_no_scroll(text.into_bytes());
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
2023-12-17 17:49:39 -07:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
TermEvent::ResetTitle => {
|
|
|
|
|
self.tab_model.text_set(entity, "New Terminal");
|
|
|
|
|
return self.update_title();
|
|
|
|
|
}
|
|
|
|
|
TermEvent::TextAreaSizeRequest(f) => {
|
|
|
|
|
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let terminal = terminal.lock().unwrap();
|
|
|
|
|
let text = f(terminal.size().into());
|
2023-12-20 15:01:47 -07:00
|
|
|
terminal.input_no_scroll(text.into_bytes());
|
2023-12-17 22:51:50 -07:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
TermEvent::Title(title) => {
|
|
|
|
|
self.tab_model.text_set(entity, title);
|
|
|
|
|
return self.update_title();
|
|
|
|
|
}
|
2023-12-20 14:26:31 -07:00
|
|
|
TermEvent::MouseCursorDirty | TermEvent::Wakeup => {
|
2023-12-20 13:31:10 -07:00
|
|
|
if let Some(terminal) = self.tab_model.data::<Mutex<Terminal>>(entity) {
|
|
|
|
|
let mut terminal = terminal.lock().unwrap();
|
|
|
|
|
terminal.update();
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
_ => {
|
|
|
|
|
println!("TODO: {:?}", event);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
Message::TermEventTx(term_event_tx) => {
|
|
|
|
|
self.term_event_tx_opt = Some(term_event_tx);
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
Command::none()
|
|
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
|
2023-12-21 10:19:16 -07:00
|
|
|
fn header_start(&self) -> Vec<Element<Self::Message>> {
|
|
|
|
|
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing;
|
|
|
|
|
|
|
|
|
|
vec![row![
|
|
|
|
|
widget::button(widget::icon::from_name("list-add-symbolic").size(16).icon())
|
|
|
|
|
.on_press(Message::TabNew)
|
|
|
|
|
.padding(space_xxs)
|
|
|
|
|
.style(style::Button::Icon)
|
|
|
|
|
]
|
|
|
|
|
.align_items(Alignment::Center)
|
|
|
|
|
.into()]
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
/// Creates a view after each update.
|
|
|
|
|
fn view(&self) -> Element<Self::Message> {
|
2023-12-21 10:29:50 -07:00
|
|
|
let cosmic_theme::Spacing { space_xxs, .. } = self.core().system_theme().cosmic().spacing;
|
2023-12-20 13:31:10 -07:00
|
|
|
|
2023-12-21 10:29:50 -07:00
|
|
|
let mut tab_column = widget::column::with_capacity(1);
|
2023-12-20 13:31:10 -07:00
|
|
|
|
2023-12-21 10:19:16 -07:00
|
|
|
if self.tab_model.iter().count() > 1 {
|
|
|
|
|
tab_column = tab_column.push(
|
2023-12-22 11:09:31 -07:00
|
|
|
widget::container(
|
2023-12-21 12:05:05 -07:00
|
|
|
widget::view_switcher::horizontal(&self.tab_model)
|
|
|
|
|
.button_height(32)
|
|
|
|
|
.button_spacing(space_xxs)
|
|
|
|
|
.on_activate(Message::TabActivate)
|
2023-12-22 11:09:31 -07:00
|
|
|
.on_close(Message::TabClose),
|
|
|
|
|
)
|
|
|
|
|
.style(style::Container::Background)
|
|
|
|
|
.width(Length::Fill),
|
2023-12-21 10:19:16 -07:00
|
|
|
);
|
|
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
|
|
|
|
|
match self
|
|
|
|
|
.tab_model
|
|
|
|
|
.data::<Mutex<Terminal>>(self.tab_model.active())
|
|
|
|
|
{
|
|
|
|
|
Some(terminal) => {
|
|
|
|
|
//TODO
|
|
|
|
|
tab_column = tab_column.push(terminal_box(terminal));
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
None => {
|
|
|
|
|
//TODO
|
2023-12-18 13:54:08 -07:00
|
|
|
}
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
2023-12-17 17:49:39 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
let content: Element<_> = tab_column.into();
|
2023-12-18 09:41:00 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
// Uncomment to debug layout:
|
|
|
|
|
//content.explain(cosmic::iced::Color::WHITE)
|
|
|
|
|
content
|
|
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
fn subscription(&self) -> Subscription<Self::Message> {
|
|
|
|
|
struct TerminalEventWorker;
|
2023-12-21 22:13:17 -07:00
|
|
|
Subscription::batch([
|
|
|
|
|
event::listen_with(|event, _status| match event {
|
|
|
|
|
Event::Keyboard(KeyEvent::KeyPressed {
|
|
|
|
|
key_code: KeyCode::C,
|
|
|
|
|
modifiers,
|
|
|
|
|
}) => {
|
|
|
|
|
if modifiers == Modifiers::CTRL | Modifiers::SHIFT {
|
|
|
|
|
Some(Message::Copy)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
2023-12-17 17:49:39 -07:00
|
|
|
}
|
2023-12-22 11:09:31 -07:00
|
|
|
Event::Keyboard(KeyEvent::KeyPressed {
|
|
|
|
|
key_code: KeyCode::T,
|
|
|
|
|
modifiers,
|
|
|
|
|
}) => {
|
|
|
|
|
if modifiers == Modifiers::CTRL | Modifiers::SHIFT {
|
|
|
|
|
Some(Message::TabNew)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-21 22:13:17 -07:00
|
|
|
Event::Keyboard(KeyEvent::KeyPressed {
|
|
|
|
|
key_code: KeyCode::V,
|
|
|
|
|
modifiers,
|
|
|
|
|
}) => {
|
|
|
|
|
if modifiers == Modifiers::CTRL | Modifiers::SHIFT {
|
|
|
|
|
Some(Message::Paste)
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
_ => None,
|
|
|
|
|
}),
|
|
|
|
|
subscription::channel(
|
|
|
|
|
TypeId::of::<TerminalEventWorker>(),
|
|
|
|
|
100,
|
|
|
|
|
|mut output| async move {
|
|
|
|
|
let (event_tx, mut event_rx) = mpsc::channel(100);
|
|
|
|
|
output.send(Message::TermEventTx(event_tx)).await.unwrap();
|
|
|
|
|
|
|
|
|
|
// Create first terminal tab
|
|
|
|
|
output.send(Message::TabNew).await.unwrap();
|
|
|
|
|
|
|
|
|
|
while let Some((entity, event)) = event_rx.recv().await {
|
|
|
|
|
output
|
|
|
|
|
.send(Message::TermEvent(entity, event))
|
|
|
|
|
.await
|
|
|
|
|
.unwrap();
|
|
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
|
2023-12-21 22:13:17 -07:00
|
|
|
panic!("terminal event channel closed");
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
])
|
2023-12-20 13:31:10 -07:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-17 11:53:26 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
impl App
|
|
|
|
|
where
|
|
|
|
|
Self: cosmic::Application,
|
|
|
|
|
{
|
|
|
|
|
fn update_title(&mut self) -> Command<Message> {
|
|
|
|
|
let (header_title, window_title) = match self.tab_model.text(self.tab_model.active()) {
|
|
|
|
|
Some(tab_title) => (
|
|
|
|
|
tab_title.to_string(),
|
|
|
|
|
format!("{tab_title} — COSMIC Terminal"),
|
|
|
|
|
),
|
2023-12-21 09:49:32 -07:00
|
|
|
None => (String::new(), "COSMIC Terminal".to_string()),
|
2023-12-20 13:31:10 -07:00
|
|
|
};
|
|
|
|
|
self.set_header_title(header_title);
|
|
|
|
|
self.set_window_title(window_title)
|
|
|
|
|
}
|
2023-12-17 11:53:26 -07:00
|
|
|
}
|