Implement open in terminal
This commit is contained in:
parent
49d4dea8b4
commit
ef040c4277
8 changed files with 238 additions and 58 deletions
83
src/app.rs
83
src/app.rs
|
|
@ -13,7 +13,10 @@ use cosmic::{
|
|||
window, Alignment, Event, Length,
|
||||
},
|
||||
style, theme,
|
||||
widget::{self, segmented_button},
|
||||
widget::{
|
||||
self,
|
||||
segmented_button::{self, Entity},
|
||||
},
|
||||
Application, ApplicationExt, Element,
|
||||
};
|
||||
use notify::Watcher;
|
||||
|
|
@ -30,8 +33,9 @@ use crate::{
|
|||
config::{AppTheme, Config, IconSizes, TabConfig, CONFIG_VERSION},
|
||||
fl, home_dir,
|
||||
key_bind::{key_binds, KeyBind},
|
||||
menu,
|
||||
menu, mime_app,
|
||||
operation::Operation,
|
||||
spawn_detached::spawn_detached,
|
||||
tab::{self, ItemMetadata, Location, Tab},
|
||||
};
|
||||
|
||||
|
|
@ -58,6 +62,7 @@ pub enum Action {
|
|||
NewFile,
|
||||
NewFolder,
|
||||
Open,
|
||||
OpenTerminal,
|
||||
OpenWith,
|
||||
Operations,
|
||||
Paste,
|
||||
|
|
@ -78,7 +83,7 @@ pub enum Action {
|
|||
}
|
||||
|
||||
impl Action {
|
||||
pub fn message(self, entity_opt: Option<segmented_button::Entity>) -> Message {
|
||||
pub fn message(self, entity_opt: Option<Entity>) -> Message {
|
||||
match self {
|
||||
Action::About => Message::ToggleContextPage(ContextPage::About),
|
||||
Action::Copy => Message::Copy(entity_opt),
|
||||
|
|
@ -95,6 +100,7 @@ impl Action {
|
|||
Action::NewFile => Message::NewItem(entity_opt, false),
|
||||
Action::NewFolder => Message::NewItem(entity_opt, true),
|
||||
Action::Open => Message::TabMessage(entity_opt, tab::Message::Open),
|
||||
Action::OpenTerminal => Message::OpenTerminal(entity_opt),
|
||||
Action::OpenWith => Message::ToggleContextPage(ContextPage::OpenWith),
|
||||
Action::Operations => Message::ToggleContextPage(ContextPage::Operations),
|
||||
Action::Paste => Message::Paste(entity_opt),
|
||||
|
|
@ -125,34 +131,35 @@ impl Action {
|
|||
pub enum Message {
|
||||
AppTheme(AppTheme),
|
||||
Config(Config),
|
||||
Copy(Option<segmented_button::Entity>),
|
||||
Cut(Option<segmented_button::Entity>),
|
||||
Copy(Option<Entity>),
|
||||
Cut(Option<Entity>),
|
||||
DialogCancel,
|
||||
DialogComplete,
|
||||
DialogUpdate(DialogPage),
|
||||
EditLocation(Option<segmented_button::Entity>),
|
||||
EditLocation(Option<Entity>),
|
||||
Key(Modifiers, Key),
|
||||
LaunchUrl(String),
|
||||
Modifiers(Modifiers),
|
||||
MoveToTrash(Option<segmented_button::Entity>),
|
||||
NewItem(Option<segmented_button::Entity>, bool),
|
||||
MoveToTrash(Option<Entity>),
|
||||
NewItem(Option<Entity>, bool),
|
||||
NotifyEvent(notify::Event),
|
||||
NotifyWatcher(WatcherWrapper),
|
||||
Paste(Option<segmented_button::Entity>),
|
||||
OpenTerminal(Option<Entity>),
|
||||
Paste(Option<Entity>),
|
||||
PendingComplete(u64),
|
||||
PendingError(u64, String),
|
||||
PendingProgress(u64, f32),
|
||||
Rename(Option<segmented_button::Entity>),
|
||||
RestoreFromTrash(Option<segmented_button::Entity>),
|
||||
Rename(Option<Entity>),
|
||||
RestoreFromTrash(Option<Entity>),
|
||||
SystemThemeModeChange(cosmic_theme::ThemeMode),
|
||||
TabActivate(segmented_button::Entity),
|
||||
TabActivate(Entity),
|
||||
TabNext,
|
||||
TabPrev,
|
||||
TabClose(Option<segmented_button::Entity>),
|
||||
TabClose(Option<Entity>),
|
||||
TabConfig(TabConfig),
|
||||
TabMessage(Option<segmented_button::Entity>, tab::Message),
|
||||
TabMessage(Option<Entity>, tab::Message),
|
||||
TabNew,
|
||||
TabRescan(segmented_button::Entity, Vec<tab::Item>),
|
||||
TabRescan(Entity, Vec<tab::Item>),
|
||||
ToggleContextPage(ContextPage),
|
||||
WindowClose,
|
||||
WindowNew,
|
||||
|
|
@ -256,11 +263,7 @@ impl App {
|
|||
self.pending_operations.insert(id, (operation, 0.0));
|
||||
}
|
||||
|
||||
fn rescan_tab(
|
||||
&mut self,
|
||||
entity: segmented_button::Entity,
|
||||
location: Location,
|
||||
) -> Command<Message> {
|
||||
fn rescan_tab(&mut self, entity: Entity, location: Location) -> Command<Message> {
|
||||
let icon_sizes = self.config.tab.icon_sizes;
|
||||
Command::perform(
|
||||
async move {
|
||||
|
|
@ -643,7 +646,7 @@ impl Application for App {
|
|||
Some(&self.nav_model)
|
||||
}
|
||||
|
||||
fn on_nav_select(&mut self, entity: segmented_button::Entity) -> Command<Self::Message> {
|
||||
fn on_nav_select(&mut self, entity: Entity) -> Command<Self::Message> {
|
||||
let location_opt = self.nav_model.data::<Location>(entity).clone();
|
||||
|
||||
if let Some(location) = location_opt {
|
||||
|
|
@ -864,6 +867,44 @@ impl Application for App {
|
|||
log::warn!("message did not contain notify watcher");
|
||||
}
|
||||
},
|
||||
Message::OpenTerminal(entity_opt) => {
|
||||
if let Some(terminal) = mime_app::terminal() {
|
||||
let mut paths = Vec::new();
|
||||
let entity = entity_opt.unwrap_or_else(|| self.tab_model.active());
|
||||
if let Some(tab) = self.tab_model.data_mut::<Tab>(entity) {
|
||||
if let Location::Path(path) = &tab.location {
|
||||
if let Some(items) = tab.items_opt() {
|
||||
for item in items.iter() {
|
||||
if item.selected {
|
||||
paths.push(item.path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if paths.is_empty() {
|
||||
paths.push(path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
for path in paths {
|
||||
if let Some(mut command) = terminal.command(None) {
|
||||
command.current_dir(&path);
|
||||
match spawn_detached(&mut command) {
|
||||
Ok(()) => {}
|
||||
Err(err) => {
|
||||
log::warn!(
|
||||
"failed to launch terminal {:?} in {:?}: {}",
|
||||
terminal.id,
|
||||
path,
|
||||
err
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::warn!("failed to get command for {:?}", terminal.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Paste(_entity_opt) => {
|
||||
log::warn!("TODO: PASTE");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ mod mime_app;
|
|||
mod mime_icon;
|
||||
mod mouse_area;
|
||||
mod operation;
|
||||
mod spawn_detached;
|
||||
mod tab;
|
||||
|
||||
pub fn home_dir() -> PathBuf {
|
||||
|
|
|
|||
20
src/menu.rs
20
src/menu.rs
|
|
@ -57,9 +57,18 @@ pub fn context_menu<'a>(
|
|||
.on_press(tab::Message::ContextAction(action))
|
||||
};
|
||||
|
||||
let selected = tab
|
||||
.items_opt()
|
||||
.map_or(0, |items| items.iter().filter(|x| x.selected).count());
|
||||
let mut selected_dir = 0;
|
||||
let mut selected = 0;
|
||||
tab.items_opt().map(|items| {
|
||||
for item in items.iter() {
|
||||
if item.selected {
|
||||
selected += 1;
|
||||
if item.metadata.is_dir() {
|
||||
selected_dir += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut children: Vec<Element<_>> = Vec::new();
|
||||
match tab.location {
|
||||
|
|
@ -68,6 +77,10 @@ pub fn context_menu<'a>(
|
|||
children.push(menu_item(fl!("open"), Action::Open).into());
|
||||
if selected == 1 {
|
||||
children.push(menu_item(fl!("open-with"), Action::OpenWith).into());
|
||||
if selected_dir == 1 {
|
||||
children
|
||||
.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
|
||||
}
|
||||
}
|
||||
children.push(horizontal_rule(1).into());
|
||||
children.push(menu_item(fl!("rename"), Action::Rename).into());
|
||||
|
|
@ -85,6 +98,7 @@ pub fn context_menu<'a>(
|
|||
//TODO: have things like properties but they apply to the folder?
|
||||
children.push(menu_item(fl!("new-file"), Action::NewFile).into());
|
||||
children.push(menu_item(fl!("new-folder"), Action::NewFolder).into());
|
||||
children.push(menu_item(fl!("open-in-terminal"), Action::OpenTerminal).into());
|
||||
children.push(horizontal_rule(1).into());
|
||||
children.push(menu_item(fl!("select-all"), Action::SelectAll).into());
|
||||
children.push(menu_item(fl!("paste"), Action::Paste).into());
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use cosmic::desktop;
|
|||
use cosmic::widget;
|
||||
pub use mime_guess::Mime;
|
||||
use once_cell::sync::Lazy;
|
||||
use std::{collections::HashMap, path::PathBuf, sync::Mutex, time::Instant};
|
||||
use std::{collections::HashMap, path::PathBuf, process, sync::Mutex, time::Instant};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct MimeApp {
|
||||
|
|
@ -18,6 +18,33 @@ pub struct MimeApp {
|
|||
pub is_default: bool,
|
||||
}
|
||||
|
||||
impl MimeApp {
|
||||
//TODO: move to libcosmic, support multiple files
|
||||
pub fn command(&self, path_opt: Option<PathBuf>) -> Option<process::Command> {
|
||||
let args_vec: Vec<String> = self.exec.as_deref().and_then(shlex::split)?;
|
||||
let mut args = args_vec.iter();
|
||||
let mut command = process::Command::new(args.next()?);
|
||||
for arg in args {
|
||||
if arg.starts_with('%') {
|
||||
match arg.as_str() {
|
||||
"%f" | "%F" | "%u" | "%U" => {
|
||||
if let Some(path) = &path_opt {
|
||||
command.arg(path);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
log::warn!("unsupported Exec code {:?} in {:?}", arg, self.id);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
command.arg(arg);
|
||||
}
|
||||
}
|
||||
Some(command)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
impl From<&desktop::DesktopEntryData> for MimeApp {
|
||||
fn from(app: &desktop::DesktopEntryData) -> Self {
|
||||
|
|
@ -46,12 +73,14 @@ fn filename_eq(path_opt: &Option<PathBuf>, filename: &str) -> bool {
|
|||
|
||||
pub struct MimeAppCache {
|
||||
cache: HashMap<Mime, Vec<MimeApp>>,
|
||||
terminals: Vec<MimeApp>,
|
||||
}
|
||||
|
||||
impl MimeAppCache {
|
||||
pub fn new() -> Self {
|
||||
let mut mime_app_cache = Self {
|
||||
cache: HashMap::new(),
|
||||
terminals: Vec::new(),
|
||||
};
|
||||
mime_app_cache.reload();
|
||||
mime_app_cache
|
||||
|
|
@ -66,6 +95,7 @@ impl MimeAppCache {
|
|||
let start = Instant::now();
|
||||
|
||||
self.cache.clear();
|
||||
self.terminals.clear();
|
||||
|
||||
//TODO: get proper locale?
|
||||
let locale = None;
|
||||
|
|
@ -83,6 +113,12 @@ impl MimeAppCache {
|
|||
apps.push(MimeApp::from(app));
|
||||
}
|
||||
}
|
||||
for category in app.categories.iter() {
|
||||
if category == "TerminalEmulator" {
|
||||
self.terminals.push(MimeApp::from(app));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load mimeapps.list files
|
||||
|
|
@ -202,3 +238,22 @@ pub fn mime_apps(mime: &Mime) -> Vec<MimeApp> {
|
|||
let mime_app_cache = MIME_APP_CACHE.lock().unwrap();
|
||||
mime_app_cache.get(mime)
|
||||
}
|
||||
|
||||
pub fn terminal() -> Option<MimeApp> {
|
||||
let mime_app_cache = MIME_APP_CACHE.lock().unwrap();
|
||||
|
||||
//TODO: consider rules in https://github.com/Vladimir-csp/xdg-terminal-exec
|
||||
|
||||
// Look for and return preferred terminals
|
||||
//TODO: fallback order beyond cosmic-term?
|
||||
for id in &["com.system76.CosmicTerm"] {
|
||||
for terminal in mime_app_cache.terminals.iter() {
|
||||
if &terminal.id == id {
|
||||
return Some(terminal.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return whatever was the first terminal found
|
||||
mime_app_cache.terminals.first().map(|x| x.clone())
|
||||
}
|
||||
|
|
|
|||
37
src/spawn_detached.rs
Normal file
37
src/spawn_detached.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use std::{io, process};
|
||||
|
||||
// This code is from the open crate and retains its MIT license.
|
||||
pub fn spawn_detached(command: &mut process::Command) -> io::Result<()> {
|
||||
command
|
||||
.stdin(process::Stdio::null())
|
||||
.stdout(process::Stdio::null())
|
||||
.stderr(process::Stdio::null());
|
||||
|
||||
#[cfg(unix)]
|
||||
unsafe {
|
||||
use std::os::unix::process::CommandExt as _;
|
||||
|
||||
command.pre_exec(move || {
|
||||
match libc::fork() {
|
||||
-1 => return Err(io::Error::last_os_error()),
|
||||
0 => (),
|
||||
_ => libc::_exit(0),
|
||||
}
|
||||
|
||||
if libc::setsid() == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
const CREATE_NEW_PROCESS_GROUP: u32 = 0x00000200;
|
||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||
command.creation_flags(CREATE_NEW_PROCESS_GROUP | CREATE_NO_WINDOW);
|
||||
}
|
||||
|
||||
command.spawn().map(|_| ())
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue