use super::*;
#[derive(Debug, PartialEq, Eq)]
pub struct PanelSession {
pub tabs: Vec<PathBuf>,
pub active: usize,
}
impl PanelSession {
pub fn dir(&self) -> &std::path::Path {
&self.tabs[self.active]
}
}
pub fn load_session() -> Option<(PanelSession, PanelSession)> {
let text = fs::read_to_string(config_path("session")?).ok()?;
parse_session(&text)
}
pub fn save_session(left: &PanelSession, right: &PanelSession) -> io::Result<()> {
let mut text = format!("left = {}\nright = {}\n", escape_path(left.dir()), escape_path(right.dir()));
for (side, panel) in [("left", left), ("right", right)] {
for tab in &panel.tabs {
text.push_str(&format!("{side}_tab = {}\n", escape_path(tab)));
}
text.push_str(&format!("{side}_tab_active = {}\n", panel.active));
}
write_config_file("session", &text)
}
pub fn load_bookmarks() -> Vec<PathBuf> {
config_path("bookmarks").and_then(|path| fs::read_to_string(path).ok()).map(|text| parse_bookmarks(&text)).unwrap_or_default()
}
pub fn save_bookmarks(bookmarks: &[PathBuf]) -> io::Result<()> {
let text: String = bookmarks.iter().map(|path| format!("bookmark = {}\n", escape_path(path))).collect();
write_config_file("bookmarks", &text)
}
pub(super) fn parse_bookmarks(text: &str) -> Vec<PathBuf> {
key_values(text).filter(|&(key, value)| key == "bookmark" && !value.is_empty()).map(|(_, value)| unescape_path(value)).collect()
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct MenuEntry {
pub label: String,
pub command: String,
}
const MENU_TEMPLATE: &str = "\
# fm84's user menu, opened with Alt+M. A command a line, as
# label = command
# Each runs in the shell, in the active panel's directory, with fm84 out of
# the way until Enter is pressed after it. In the command:
# {file} the entry under the cursor {name} its name alone
# {files} what is chosen, the selection or else the entry under the cursor
# {dir} the panel's directory {other} the other panel's
# Leave them unquoted: each is one word to the shell, spaces and all - and
# {files} one word for each.
#
# Disk use = du -sh {files}
# Git log = git log --oneline --graph -20
# Pack as tar.gz = tar czf {name}.tar.gz {files}
# Diff with the other panel = diff -r {dir} {other}
";
pub fn load_menu() -> Vec<MenuEntry> {
config_path("menu").and_then(|path| fs::read_to_string(path).ok()).map(|text| parse_menu(&text)).unwrap_or_default()
}
pub(super) fn parse_menu(text: &str) -> Vec<MenuEntry> {
key_values(text)
.filter(|(label, command)| !label.is_empty() && !command.is_empty())
.map(|(label, command)| MenuEntry {
label: label.to_string(),
command: command.to_string(),
})
.collect()
}
pub fn menu_file() -> Option<PathBuf> {
let path = config_path("menu")?;
if !path.exists() {
let _ = write_config_file("menu", MENU_TEMPLATE);
}
Some(path)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Association {
pub extensions: Vec<String>,
pub command: String,
pub terminal: bool,
}
const OPEN_TEMPLATE: &str = "\
# The programs fm84 opens files with, by extension, as
# extensions = command
# The first line for a file is what Enter and Shift+Enter open it with, in
# place of the system's choice; Alt+O lists them all, then what the system
# offers. Extensions are separated by spaces, any case, without the dot -
# tar.gz works too - and * is every file. The command runs in the shell, in
# the file's directory, with {file} where the file goes, or on the end if it
# is not there; {name} {dir} {other} as in the user menu, and unquoted.
# A ! in front runs it in this terminal, fm84 waiting until it is done.
#
# pdf epub = zathura
# jpg jpeg png gif webp = gimp
# md txt = !vim
# * = !less
";
pub fn load_associations() -> Vec<Association> {
config_path("open").and_then(|path| fs::read_to_string(path).ok()).map(|text| parse_associations(&text)).unwrap_or_default()
}
pub(super) fn parse_associations(text: &str) -> Vec<Association> {
key_values(text)
.filter_map(|(extensions, command)| {
let extensions: Vec<String> = extensions.split([' ', '\t', ',']).map(|extension| extension.trim_start_matches('.')).filter(|extension| !extension.is_empty()).map(String::from).collect();
let (command, terminal) = match command.strip_prefix('!') {
Some(command) => (command.trim(), true),
None => (command, false),
};
(!extensions.is_empty() && !command.is_empty()).then(|| Association {
extensions,
command: command.to_string(),
terminal,
})
})
.collect()
}
pub fn associations_file() -> Option<PathBuf> {
let path = config_path("open")?;
if !path.exists() {
let _ = write_config_file("open", OPEN_TEMPLATE);
}
Some(path)
}
pub(super) fn escape_path(path: &std::path::Path) -> String {
let escaped = |bytes: &[u8]| bytes.iter().map(|byte| format!("%{byte:02X}")).collect::<String>();
let mut out = String::new();
for chunk in path.as_os_str().as_encoded_bytes().utf8_chunks() {
for ch in chunk.valid().chars() {
if ch == '%' || ch.is_control() {
out.push_str(&escaped(ch.encode_utf8(&mut [0; 4]).as_bytes()));
} else {
out.push(ch);
}
}
out.push_str(&escaped(chunk.invalid()));
}
let start = out.len() - out.trim_start().len();
let end = start + out.trim().len();
format!("{}{}{}", escaped(&out.as_bytes()[..start]), &out[start..end], escaped(&out.as_bytes()[end..]))
}
pub(super) fn unescape_path(text: &str) -> PathBuf {
let bytes = text.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut index = 0;
while index < bytes.len() {
let hex = bytes.get(index + 1..index + 3).filter(|pair| pair.iter().all(u8::is_ascii_hexdigit)).and_then(|pair| std::str::from_utf8(pair).ok());
match (bytes[index], hex.and_then(|pair| u8::from_str_radix(pair, 16).ok())) {
(b'%', Some(byte)) => {
out.push(byte);
index += 3;
}
(byte, _) => {
out.push(byte);
index += 1;
}
}
}
path_from_bytes(out)
}
#[cfg(unix)]
fn path_from_bytes(bytes: Vec<u8>) -> PathBuf {
use std::os::unix::ffi::OsStringExt;
PathBuf::from(std::ffi::OsString::from_vec(bytes))
}
#[cfg(not(unix))]
fn path_from_bytes(bytes: Vec<u8>) -> PathBuf {
PathBuf::from(String::from_utf8_lossy(&bytes).into_owned())
}
pub(super) fn parse_session(text: &str) -> Option<(PanelSession, PanelSession)> {
let (mut left, mut right) = (None, None);
let (mut left_tabs, mut right_tabs) = (Vec::new(), Vec::new());
let (mut left_active, mut right_active) = (0, 0);
for (key, value) in key_values(text) {
match key {
"left" if !value.is_empty() => left = Some(unescape_path(value)),
"right" if !value.is_empty() => right = Some(unescape_path(value)),
"left_tab" if !value.is_empty() => left_tabs.push(unescape_path(value)),
"right_tab" if !value.is_empty() => right_tabs.push(unescape_path(value)),
"left_tab_active" => left_active = value.parse().unwrap_or(0),
"right_tab_active" => right_active = value.parse().unwrap_or(0),
_ => {}
}
}
let panel = |dir: PathBuf, tabs: Vec<PathBuf>, active: usize| {
if tabs.is_empty() {
PanelSession { tabs: vec![dir], active: 0 }
} else {
let active = active.min(tabs.len() - 1);
PanelSession { tabs, active }
}
};
Some((panel(left?, left_tabs, left_active), panel(right?, right_tabs, right_active)))
}