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::{
|
|
|
|
|
app::{Command, Core, Settings},
|
|
|
|
|
cosmic_theme, executor,
|
|
|
|
|
iced::{
|
|
|
|
|
futures::SinkExt,
|
|
|
|
|
subscription::{self, Subscription},
|
|
|
|
|
widget::row,
|
|
|
|
|
window, Alignment, 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 {
|
|
|
|
|
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;
|
|
|
|
|
|
|
|
|
|
/// Argument received [`cosmic::Application::new`].
|
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 10:14:57 -07:00
|
|
|
fn init(core: Core, term_config: Self::Flags) -> (Self, Command<Self::Message>) {
|
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 09:49:32 -07:00
|
|
|
terminal_theme: "OneHalfDark".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 {
|
|
|
|
|
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-20 13:31:10 -07:00
|
|
|
/// Creates a view after each update.
|
|
|
|
|
fn view(&self) -> Element<Self::Message> {
|
2023-12-20 14:57:44 -07:00
|
|
|
let cosmic_theme::Spacing {
|
|
|
|
|
space_none,
|
|
|
|
|
space_xxs,
|
|
|
|
|
..
|
|
|
|
|
} = self.core().system_theme().cosmic().spacing;
|
2023-12-20 13:31:10 -07:00
|
|
|
|
2023-12-20 14:57:44 -07:00
|
|
|
let mut tab_column = widget::column::with_capacity(1).padding([space_none, space_xxs]);
|
2023-12-20 13:31:10 -07:00
|
|
|
|
|
|
|
|
tab_column = tab_column.push(
|
|
|
|
|
row![
|
|
|
|
|
widget::view_switcher::horizontal(&self.tab_model)
|
|
|
|
|
.button_height(32)
|
|
|
|
|
.button_spacing(space_xxs)
|
|
|
|
|
.on_activate(Message::TabActivate)
|
|
|
|
|
.on_close(Message::TabClose)
|
|
|
|
|
.width(Length::Shrink),
|
|
|
|
|
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),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
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-17 17:49:39 -07:00
|
|
|
}
|
2023-12-18 13:54:08 -07:00
|
|
|
|
2023-12-20 13:31:10 -07:00
|
|
|
panic!("terminal event channel closed");
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
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
|
|
|
}
|