Skip to content

Commit afc9b9c

Browse files
committed
Add TerminalRepository
1 parent 93fc081 commit afc9b9c

File tree

5 files changed

+237
-30
lines changed

5 files changed

+237
-30
lines changed

Cargo.lock

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ anyhow = "1.0.92"
1414
regex = "1.11.1"
1515
tracing = "0.1"
1616
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
17+
serde = { version = "1.0", features = ["derive"] }
18+
serde_json = "1.0.140"
1719

1820
[dependencies.adw]
1921
package = "libadwaita"

src/store/root_store.rs

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use glib::Properties;
77
use gtk::prelude::*;
88
use gtk::{gio, glib};
99
use std::cell::RefCell;
10+
use std::default;
1011
use std::time::Duration;
1112
use tracing::debug;
1213
use tracing::error;
@@ -21,8 +22,7 @@ use crate::distrobox::Status;
2122
use crate::distrobox_task::DistroboxTask;
2223
use crate::gtk_utils::reconcile_list_by_key;
2324
use crate::remote_resource::RemoteResource;
24-
use crate::supported_terminals::SupportedTerminal;
25-
use crate::supported_terminals::SUPPORTED_TERMINALS;
25+
use crate::supported_terminals::{Terminal, TerminalRepository};
2626
use crate::tagged_object::TaggedObject;
2727

2828
mod imp {
@@ -36,6 +36,8 @@ mod imp {
3636
#[properties(wrapper_type = super::RootStore)]
3737
pub struct RootStore {
3838
pub distrobox: OnceCell<crate::distrobox::Distrobox>,
39+
pub terminal_repository: TerminalRepository,
40+
3941
#[property(get, set)]
4042
pub distrobox_version: RefCell<RemoteResource>,
4143

@@ -65,6 +67,7 @@ mod imp {
6567
fn default() -> Self {
6668
Self {
6769
containers: gio::ListStore::new::<crate::container::Container>(),
70+
terminal_repository: Default::default(),
6871
selected_container: Default::default(),
6972
current_view: Default::default(),
7073
current_dialog: Default::default(),
@@ -129,6 +132,19 @@ impl RootStore {
129132
}
130133
}));
131134

135+
if this.selected_terminal().is_none() {
136+
let this = this.clone();
137+
glib::MainContext::ref_thread_default().spawn_local(async move {
138+
let Some(default_terminal) = this
139+
.terminal_repository()
140+
.default_terminal()
141+
.await else {
142+
return;
143+
};
144+
this.set_selected_terminal_name(&default_terminal.name);
145+
});
146+
}
147+
132148
this.load_containers();
133149
this
134150
}
@@ -137,6 +153,10 @@ impl RootStore {
137153
self.imp().distrobox.get().unwrap()
138154
}
139155

156+
pub fn terminal_repository(&self) -> &TerminalRepository {
157+
&self.imp().terminal_repository
158+
}
159+
140160
pub fn load_containers(&self) {
141161
let this = self.clone();
142162
glib::MainContext::ref_thread_default().spawn_local_with_priority(
@@ -256,21 +276,32 @@ impl RootStore {
256276
}
257277
Ok(())
258278
}
259-
pub fn selected_terminal(&self) -> Option<SupportedTerminal> {
260-
let program: String = self.settings().string("selected-terminal").into();
261-
SUPPORTED_TERMINALS
262-
.iter()
263-
.find(|x| x.program == program)
264-
.cloned()
265-
}
266-
pub fn set_selected_terminal_program(&self, program: &str) {
267-
if !SUPPORTED_TERMINALS.iter().any(|x| x.program == program) {
268-
panic!("Unsupported terminal");
279+
pub fn selected_terminal(&self) -> Option<Terminal> {
280+
// Old version stored the program, such as "gnome-terminal", now we store the name "GNOME console".
281+
let name_or_program: String = self.settings().string("selected-terminal").into();
282+
283+
let by_name = self
284+
.imp()
285+
.terminal_repository
286+
.terminal_by_name(&name_or_program);
287+
288+
if let Some(terminal) = by_name {
289+
Some(terminal)
290+
} else if let Some(terminal) = self
291+
.imp()
292+
.terminal_repository
293+
.terminal_by_program(&name_or_program)
294+
{
295+
Some(terminal)
296+
} else {
297+
error!("Terminal not found: {}", name_or_program);
298+
None
269299
}
270-
300+
}
301+
pub fn set_selected_terminal_name(&self, name: &str) {
271302
self.imp()
272303
.settings
273-
.set_string("selected-terminal", program)
304+
.set_string("selected-terminal", name)
274305
.expect("Failed to save setting");
275306
}
276307

src/supported_terminals.rs

Lines changed: 156 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
1-
use std::sync::LazyLock;
1+
use std::{
2+
cell::RefCell,
3+
path::{Path, PathBuf},
4+
sync::LazyLock,
5+
};
26

3-
#[derive(Clone, Debug)]
4-
pub struct SupportedTerminal {
7+
use gtk::glib;
8+
use tracing::{error, info};
9+
10+
use crate::{
11+
config,
12+
distrobox::{Command, CommandRunner, RealCommandRunner},
13+
};
14+
15+
use gtk::subclass::prelude::*;
16+
17+
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
18+
pub struct Terminal {
519
pub name: String,
620
pub program: String,
721
pub separator_arg: String,
22+
pub read_only: bool,
823
}
924

10-
pub static SUPPORTED_TERMINALS: LazyLock<Vec<SupportedTerminal>> = LazyLock::new(|| {
25+
static SUPPORTED_TERMINALS: LazyLock<Vec<Terminal>> = LazyLock::new(|| {
1126
[
1227
("GNOME Console", "kgx", "--"),
1328
("GNOME Terminal", "gnome-terminal", "--"),
@@ -26,14 +41,148 @@ pub static SUPPORTED_TERMINALS: LazyLock<Vec<SupportedTerminal>> = LazyLock::new
2641
("Terminator", "terminator", "-x"),
2742
]
2843
.iter()
29-
.map(|(name, program, separator_arg)| SupportedTerminal {
44+
.map(|(name, program, separator_arg)| Terminal {
3045
name: name.to_string(),
3146
program: program.to_string(),
3247
separator_arg: separator_arg.to_string(),
48+
read_only: true,
3349
})
3450
.collect()
3551
});
3652

37-
pub fn terminal_by_name(name: &str) -> Option<SupportedTerminal> {
38-
SUPPORTED_TERMINALS.iter().find(|x| x.name == name).cloned()
53+
mod imp {
54+
use super::*;
55+
use std::cell::RefCell;
56+
57+
pub struct TerminalRepository {
58+
pub list: RefCell<Vec<Terminal>>,
59+
pub custom_list_path: PathBuf,
60+
pub command_runner: RefCell<Box<dyn CommandRunner>>,
61+
}
62+
63+
impl Default for TerminalRepository {
64+
fn default() -> Self {
65+
let custom_list_path = glib::user_data_dir().join("distroshelf-terminals.json");
66+
Self {
67+
list: RefCell::new(vec![]),
68+
custom_list_path,
69+
command_runner: RefCell::new(Box::new(RealCommandRunner {})),
70+
}
71+
}
72+
}
73+
impl ObjectImpl for TerminalRepository {}
74+
75+
#[glib::object_subclass]
76+
impl ObjectSubclass for TerminalRepository {
77+
const NAME: &'static str = "TerminalRepository";
78+
type Type = super::TerminalRepository;
79+
}
80+
}
81+
82+
83+
glib::wrapper! {
84+
pub struct TerminalRepository(ObjectSubclass<imp::TerminalRepository>);
85+
}
86+
87+
impl TerminalRepository {
88+
pub fn new() -> Self {
89+
let this: Self = glib::Object::builder()
90+
.build();
91+
92+
let mut list = SUPPORTED_TERMINALS.clone();
93+
if let Ok(loaded_list) = Self::load_terminals_from_json(&this.imp().custom_list_path) {
94+
list.extend(loaded_list);
95+
} else {
96+
error!("Failed to load custom terminals from JSON file {:?}", &this.imp().custom_list_path);
97+
}
98+
99+
list.sort_by(|a, b| a.name.cmp(&b.name));
100+
this.imp().list.replace(list);
101+
this
102+
}
103+
104+
pub fn is_read_only(&self, name: &str) -> bool {
105+
self.imp().list
106+
.borrow()
107+
.iter()
108+
.find(|x| x.name == name)
109+
.map_or(false, |x| x.read_only)
110+
}
111+
112+
pub fn save_terminal(&self, terminal: Terminal) -> anyhow::Result<()> {
113+
if self.is_read_only(terminal.name.as_str()) {
114+
return Err(anyhow::anyhow!("Cannot modify read-only terminal"));
115+
}
116+
let mut list = self.imp().list.borrow_mut();
117+
list.retain(|x| x.name != terminal.name);
118+
list.push(terminal);
119+
120+
list.sort_by(|a, b| a.name.cmp(&b.name));
121+
122+
self.save_terminals_to_json();
123+
Ok(())
124+
}
125+
126+
pub fn delete_terminal(&self, name: &str) -> anyhow::Result<()> {
127+
if self.is_read_only(name) {
128+
return Err(anyhow::anyhow!("Cannot modify read-only terminal"));
129+
}
130+
self.imp().list.borrow_mut().retain(|x| x.name != name);
131+
Ok(())
132+
}
133+
134+
pub fn terminal_by_name(&self, name: &str) -> Option<Terminal> {
135+
self.imp().list.borrow().iter().find(|x| x.name == name).cloned()
136+
}
137+
138+
pub fn terminal_by_program(&self, program: &str) -> Option<Terminal> {
139+
self.imp().list
140+
.borrow()
141+
.iter()
142+
.find(|x| x.program == program)
143+
.cloned()
144+
}
145+
146+
pub fn all_terminals(&self) -> Vec<Terminal> {
147+
self.imp().list.borrow().clone()
148+
}
149+
150+
fn save_terminals_to_json(&self) {
151+
let list: Vec<Terminal> = self.imp().list.borrow().iter().filter(|x| !x.read_only).cloned().collect::<Vec<_>>();
152+
let json = serde_json::to_string(&*list).unwrap();
153+
std::fs::write(&self.imp().custom_list_path, json).unwrap();
154+
}
155+
156+
fn load_terminals_from_json(path: &Path) -> anyhow::Result<Vec<Terminal>> {
157+
let data = std::fs::read_to_string(path)?;
158+
let list: Vec<Terminal> = serde_json::from_str(&data)?;
159+
Ok(list)
160+
}
161+
162+
pub async fn default_terminal(&self) -> Option<Terminal> {
163+
let command = Command::new_with_args("flatpak-spawn", &["--host", "--", "get",
164+
"org.gnome.desktop.default-applications.terminal",
165+
"exec",]);
166+
let output = self.imp().command_runner.borrow().output(command.clone());
167+
let Ok(output) = output.await else {
168+
error!("Failed to get default terminal, running {:?}", &command);
169+
return None;
170+
};
171+
let terminal_program = String::from_utf8(output.stdout).unwrap().trim().to_string();
172+
if terminal_program.is_empty() {
173+
return None;
174+
}
175+
info!("Default terminal program: {}", terminal_program);
176+
self.terminal_by_program(&terminal_program)
177+
.or_else(|| {
178+
error!("Terminal program {} not found in the list", terminal_program);
179+
None
180+
})
181+
}
182+
}
183+
184+
impl Default for TerminalRepository {
185+
fn default() -> Self {
186+
Self::new()
187+
}
39188
}

0 commit comments

Comments
 (0)