Skip to content

Commit

Permalink
feat: cross platform terminal settings (#22)
Browse files Browse the repository at this point in the history
* feat: handle cross platform terminal and use local storage

* feat: handle cross platform terminal and use local storage

* feat: improve the code and made review adjsutments

* fix: error handler in command output
  • Loading branch information
hcavarsan authored Sep 25, 2024
1 parent 50609dd commit 2397788
Show file tree
Hide file tree
Showing 11 changed files with 500 additions and 603 deletions.
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;
120 changes: 120 additions & 0 deletions src-tauri/src/utils/terminal.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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])
};

let output = Command::new(shell)
.args(&args)
.output()
.map_err(|err| format!("Failed to execute terminal command: {}", err))?;

if !output.stderr.is_empty() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("Terminal command error: {}", stderr));
}

Ok(format!("Opening terminal with command: '{}'", command))
}
23 changes: 0 additions & 23 deletions src-tauri/src/utils/utils.rs

This file was deleted.

Loading

0 comments on commit 2397788

Please sign in to comment.