Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: cross platform terminal settings #22

Merged
merged 4 commits into from
Sep 25, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ dist-ssr
*.njsproj
*.sln
*.sw?
package-lock.json
70 changes: 6 additions & 64 deletions src-tauri/src/commands/container.rs
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
use crate::constants::DOCKER_TERMINAL_APP;
use crate::state::AppState;
use crate::utils::find_terminal;
use crate::utils::storage::get_storage_path;
use crate::utils::terminal::{get_terminal, open_terminal};
use bollard::container::{ListContainersOptions, LogsOptions, StatsOptions};
use bollard::models::{ContainerInspectResponse, ContainerSummary};
use futures_util::StreamExt;
use std::collections::HashMap;
use std::sync::atomic::Ordering;
use tauri::Manager;
use tauri_plugin_store::StoreBuilder;

#[tauri::command]
pub async fn fetch_containers(
Expand All @@ -26,7 +23,6 @@ pub async fn fetch_containers(
.await
.map_err(|e| e.to_string())
}

#[tauri::command]
pub async fn get_container(
state: tauri::State<'_, AppState>,
Expand Down Expand Up @@ -106,6 +102,7 @@ pub async fn container_operation(
op_type: String,
) -> Result<String, String> {
let docker = state.docker.clone();
let terminal = get_terminal(&app_handle).await?;

let mut list_container_filters = std::collections::HashMap::new();
list_container_filters.insert(String::from("name"), vec![container_name.clone()]);
Expand Down Expand Up @@ -145,7 +142,10 @@ pub async fn container_operation(
Err(e) => Err(format!("Failed to restart container: {}", e.to_string())),
},
"web" => open_container_url(container),
"exec" => open_container_shell(app_handle, container_name),
"exec" => match open_terminal(&terminal, Some("exec"), Some(&container_name)) {
Ok(_) => Ok("Opening terminal".to_string()),
Err(e) => Err(format!("Failed to open terminal: {}", e.to_string())),
},
_ => Err("Invalid operation type".to_string()),
};

Expand All @@ -165,64 +165,6 @@ fn open_container_url(container: ContainerSummary) -> Result<String, String> {
}
}

fn open_container_shell(app_handle: tauri::AppHandle, container_name: String) -> Result<String, String> {

let term_commands_prefix: HashMap<String, String> = HashMap::from([
("gnome-terminal".to_owned(), "--".to_owned()),
("alacritty".to_owned(), "-e".to_owned()),
("xterm".to_owned(), "-e".to_owned()),
("terminator".to_owned(), "-x".to_owned()),
("konsole".to_owned(), "-e".to_owned()),
]);



let mut store = StoreBuilder::new(app_handle.clone(), get_storage_path()).build();

// Attempt to load the store, if it's saved already.
store.load().map_err(|_| "Failed to load store from disk")?;

let term_app;

let stored_val = store.get(DOCKER_TERMINAL_APP);

if stored_val.is_some_and(|val| val != "") {
term_app = stored_val.unwrap().to_string().replace("\"", "");
}
else{
term_app = find_terminal().unwrap();
}

let docker_commands = vec![
"docker".to_owned(),
"exec".to_owned(),
"-it".to_owned(),
container_name.to_owned(),
"sh".to_owned(),
];

let mut command = std::process::Command::new(term_app.clone());


let term_arg = term_commands_prefix
.get(term_app.as_str())
.ok_or_else(|| format!("Terminal application '{}' not supported", term_app))?;

command.args(std::iter::once(term_arg.to_owned()).chain(docker_commands));

match command.spawn() {
Ok(_) => Ok(format!("Opening terminal inside '{container_name}'")),
Err(err) => {
match err.kind() {
std::io::ErrorKind::NotFound => Err(format!("cannot use '{}' to open terminal. Change it in settings.", term_app)),

_ => Err(format!("Cannot run exec command: {}", err.kind().to_string())),
}

},
}
}

#[tauri::command]
pub async fn container_stats(
state: tauri::State<'_, AppState>,
Expand Down
5 changes: 3 additions & 2 deletions src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
pub mod container;
pub mod extra;
pub mod image;
pub mod volume;
pub mod network;
pub mod extra;
pub mod terminal;
pub mod volume;
19 changes: 19 additions & 0 deletions src-tauri/src/commands/terminal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use crate::utils::terminal::Terminal;
#[tauri::command]
pub fn get_available_terminals() -> Vec<String> {
let current_os = if cfg!(target_os = "windows") {
"windows"
} else if cfg!(target_os = "macos") {
"macos"
} else {
"linux"
};

let variants = Terminal::variants();

variants
.iter()
.filter(|t| t.os() == current_os)
.map(|t| t.app_name().to_string())
.collect()
}
14 changes: 11 additions & 3 deletions src-tauri/src/constants.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@


pub const STORAGE_NAME: &str = "store.bin";
pub const DOCKER_TERMINAL: &str = "docker_terminal";

pub const MACOS_COMMAND_TEMPLATE: &str = r#"
osascript -e 'tell application "System Events"
do shell script "open -F -n -a {app_name}"
delay 1.0
tell application "System Events" to tell process "{app_name}" to keystroke "{cmd}" & return
end tell'
"#;

pub const DOCKER_TERMINAL_APP: &str = "docker_terminal";
pub const LINUX_COMMAND_TEMPLATE: &str = "{app_name} -e '{cmd}'";
pub const WINDOWS_COMMAND_TEMPLATE: &str = "{app_name} /C {cmd}";
6 changes: 4 additions & 2 deletions src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use crate::commands::extra::{cancel_stream, get_version, ping};
use crate::commands::image::{delete_image, export_image, image_history, image_info, list_images};
use crate::commands::network::{inspect_network, list_networks};
use crate::commands::volume::{inspect_volume, list_volumes};
use crate::commands::terminal::get_available_terminals;

mod state;
mod utils;
Expand All @@ -17,7 +18,7 @@ mod constants;

fn main() {
std::env::set_var("WEBKIT_DISABLE_COMPOSITING_MODE", "1");

let state = AppState::default();


Expand Down Expand Up @@ -47,7 +48,8 @@ fn main() {
cancel_stream,
export_image,
get_version,
ping
ping,
get_available_terminals,
])
.run(tauri::generate_context!())
.expect("error while running tauri application");
Expand Down
6 changes: 2 additions & 4 deletions src-tauri/src/utils/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
pub mod storage;

pub mod utils;

pub use utils::*;
pub mod terminal;
pub mod storage;
114 changes: 114 additions & 0 deletions src-tauri/src/utils/terminal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use crate::constants::DOCKER_TERMINAL;
use crate::constants::{LINUX_COMMAND_TEMPLATE, MACOS_COMMAND_TEMPLATE, WINDOWS_COMMAND_TEMPLATE};
use crate::utils::storage::get_storage_path;
use std::process::Command;
use tauri::Manager;
use tauri::{AppHandle, Wry};
use tauri_plugin_store::with_store;
use tauri_plugin_store::Error;
use tauri_plugin_store::StoreCollection;

macro_rules! define_terminals {
($($name:ident => $app_name:expr, $template:expr, $os:expr),*) => {
#[derive(Debug, Clone, Copy)]
pub enum Terminal {
$($name),*
}

impl Terminal {
pub fn app_name(&self) -> &'static str {
match self {
$(Terminal::$name => $app_name),*
}
}

pub fn command_template(&self) -> &'static str {
match self {
$(Terminal::$name => $template),*
}
}

pub fn os(&self) -> &'static str {
match self {
$(Terminal::$name => $os),*
}
}

pub fn from_str(s: &str) -> Result<Self, String> {
match s {
$($app_name => Ok(Terminal::$name)),*,
_ => Err(format!("Unknown terminal: {}", s)),
}
}

pub fn variants() -> &'static [Terminal] {
&[
$(Terminal::$name),*
]
}
}
};
}

define_terminals!(
GnomeTerminal => "gnome-terminal", LINUX_COMMAND_TEMPLATE, "linux",
Konsole => "konsole", LINUX_COMMAND_TEMPLATE, "linux",
Alacritty => "alacritty", LINUX_COMMAND_TEMPLATE, "linux",
Xterm => "xterm", LINUX_COMMAND_TEMPLATE, "linux",
Terminator => "terminator", LINUX_COMMAND_TEMPLATE, "linux",
Xfce4Terminal => "xfce4-terminal", LINUX_COMMAND_TEMPLATE, "linux",
Cmd => "cmd.exe", WINDOWS_COMMAND_TEMPLATE, "windows",
Powershell => "powershell.exe", WINDOWS_COMMAND_TEMPLATE, "windows",
Terminal => "Terminal", MACOS_COMMAND_TEMPLATE, "macos",
ITerm => "iTerm", MACOS_COMMAND_TEMPLATE, "macos",
WezTerm => "WezTerm", MACOS_COMMAND_TEMPLATE, "macos"
);

pub async fn get_terminal(app: &AppHandle<Wry>) -> Result<Terminal, String> {
let stores = app.state::<StoreCollection<Wry>>();
let path = get_storage_path();

let terminal_str = with_store(app.clone(), stores, path.clone(), |store| {
match store.get(DOCKER_TERMINAL) {
Some(value) => Ok(value.clone()),
None => Err(Error::NotFound(path.clone())),
}
})
.map_err(|e| format!("Failed to retrieve terminal from storage: {}", e))?;

let terminal_str = terminal_str.as_str().unwrap_or_default().to_string();

Terminal::from_str(&terminal_str).map_err(|e| format!("Invalid terminal string: {}", e))
}

pub fn open_terminal(
term_app: &Terminal,
command: Option<&str>,
container_name: Option<&str>,
) -> Result<String, String> {
let command = match command {
Some("exec") => match container_name {
Some(container) => format!("docker exec -it {} sh", container),
None => return Err("Container name must be provided for 'exec' command".to_string()),
},
Some(cmd) => cmd.to_string(),
None => return Err("No command provided".to_string()),
};

let command_template = term_app.command_template();
let shell_command = command_template
.replace("{cmd}", &command)
.replace("{app_name}", term_app.app_name());

let (shell, args) = if cfg!(target_os = "windows") {
("cmd.exe", vec!["/C", &shell_command])
} else {
("sh", vec!["-c", &shell_command])
};

Command::new(shell)
hcavarsan marked this conversation as resolved.
Show resolved Hide resolved
.args(&args)
.spawn()
.map(|_| format!("Opening terminal with command: '{}'", command))
.map_err(|err| format!("Failed to execute terminal command: {}", err))
}
23 changes: 0 additions & 23 deletions src-tauri/src/utils/utils.rs

This file was deleted.

Loading