use std::env;
#[cfg(feature = "repl")]
use {
rustyline::error::*, rustyline::DefaultEditor, sued::exit_status::ExitStatus,
sued::run_repl_command, rustyline::config::Configurer,
};
#[cfg(feature = "history")]
use {
std::fs::{self, File},
std::path::PathBuf,
};
use sued::command::{Command, CommandAction, CommandScope};
use sued::commands;
use sued::{helper, EditorState};
fn main() {
helper::startup_message();
#[cfg(feature = "repl")]
let mut rl = DefaultEditor::new().expect("Failed to create the editor");
let mut state = EditorState::new();
commands::register_all(&mut state.registry);
let prefix_cmd: Command = Command {
name: "prefix",
description: "set the command prefix",
documentation: "",
scope: CommandScope::REPLOnly,
arguments: vec!["new_prefix"],
action: CommandAction::new(|args: Vec<&str>, state: &mut EditorState| {
state.prefix.clear();
let new_prefix = args.get(1).map(|&s| s);
if let Some(prefix) = new_prefix {
if prefix.len() > 1 {
return format!("requested prefix too long");
}
if new_prefix.unwrap().chars().all(|c| c.is_alphanumeric()) {
println!("warning: chosen prefix is not a symbol");
}
state.prefix = prefix.to_string();
format!("prefix set to {}", prefix)
} else {
state.prefix = "~".to_string();
"prefix reset to ~, try passing a prefix if you wanted that instead".to_string()
}
}),
};
let clear_cmd: Command = Command {
name: "clear",
description: "empty the file buffer",
documentation: "this command will effectively reset the buffer to its \
default state, clearing the buffer contents and setting \
the file path to the None variant",
scope: CommandScope::Global,
action: CommandAction::new(|_args: Vec<&str>, state: &mut EditorState| {
state.buffer.contents.clear();
state.buffer.file_path = None;
state.cursor = 0;
"buffer cleared".to_string()
}),
..Command::default()
};
state.registry.add_command(prefix_cmd);
state.registry.add_command(clear_cmd);
let args: Vec<String> = env::args().collect();
if args.len() >= 2 {
match helper::open_file(&args[1]) {
Ok(opened) => {
if !opened.is_empty() {
let file_contents: Vec<String> =
opened.split('\n').map(|line| line.to_string()).collect();
state.buffer.contents = file_contents;
}
state.buffer.file_path = Some(args[1].clone());
let file_size_display = helper::get_file_size(&state);
println!("opened {} as text with {}", args[1], file_size_display);
}
Err(e) => {
println!("{e}");
state.buffer.file_path = Some(args[1].clone());
}
}
}
if !cfg!(feature = "repl") {
eprintln!("sued requires the `repl` feature enabled to run the text editor");
eprintln!("try appending `--features=repl` to your cargo command");
std::process::exit(1);
}
state.cursor = state.buffer.contents.len();
#[cfg(feature = "history")]
let config_path = if cfg!(windows) {
env::var("APPDATA")
.expect("APPDATA not set") .parse::<PathBuf>()
.expect("APPDATA is not a valid path")
.join("sued")
.canonicalize()
.unwrap_or_else(|_| {
let mut path = PathBuf::new();
path.push(env::var("APPDATA").unwrap());
path.push("sued");
fs::create_dir_all(&path).unwrap();
path
})
} else {
env::var("XDG_CONFIG_HOME") .unwrap_or_else(|_| "/home/".to_string())
.parse::<PathBuf>()
.expect("XDG_CONFIG_HOME is not a valid path")
.join("sued")
.canonicalize()
.unwrap_or_else(|_| {
let mut path = PathBuf::new();
path.push(
env::var("XDG_CONFIG_HOME")
.unwrap_or_else(|_| format!("{}/.config", env::var("HOME").unwrap())),
);
path.push("sued");
fs::create_dir_all(&path).unwrap();
path
})
};
#[cfg(feature = "history")]
let history_file: PathBuf = config_path.join("command-history.txt");
let mut prompt = format!("{}›", state.cursor + 1);
#[cfg(all(feature = "repl", feature = "history"))]
{
match rl.load_history(&history_file) {
Ok(()) => println!("loaded command history from {}", history_file.display()),
Err(_) => (),
}
}
#[cfg(feature = "repl")]
loop {
let line = match rl.readline_with_initial(&prompt, (&state.existing_content, "")) {
Ok(line) => line,
Err(ReadlineError::Interrupted) => {
eprintln!("use ~exit to leave sued");
continue;
}
_ => {
eprintln!("error reading input");
continue;
}
};
let command = line.trim_end().to_string();
let command_args = command.trim_start().split(' ').collect::<Vec<&str>>();
if command_args[0].starts_with(&state.prefix) {
match interpret_command(command_args, &mut state) {
ExitStatus::Success(msg) => {
let _ = rl.add_history_entry(command.clone());
println!("{}", msg);
}
ExitStatus::Failure(msg) => {
if msg.as_str() == "exit" {
#[cfg(feature = "history")]
{
if !history_file.exists() {
if let Err(e) = File::create(&history_file) {
eprintln!("error creating command history file: {}", e);
}
}
if let Err(e) = rl.set_max_history_size(100) {
eprintln!("error setting max history size: {}", e);
}
if let Err(e) = rl.save_history(&history_file) {
eprintln!("error saving command history: {}", e);
}
}
break;
} else {
eprintln!("error: {}", msg);
}
}
}
} else {
let to_write = command;
let position = state.cursor;
state.existing_content.clear();
if to_write.contains("\n") {
let to_write_split = to_write.split("\n");
for line in to_write_split {
state.buffer.contents.push(line.to_string());
state.cursor += 1;
}
} else {
state.buffer.contents.insert(position, to_write.clone());
}
let indentation = to_write.chars().take_while(|c| *c == ' ').count();
if indentation > 0 {
state.existing_content = format!("{}", " ".repeat(indentation))
}
state.cursor += 1;
}
if let Err(_) =
helper::check_if_line_in_buffer(&mut state.buffer.contents, state.cursor + 1)
{
state.cursor = state.buffer.contents.len();
}
let max_length = state.buffer.contents.len().to_string().len();
let cursor_padded: String = format!("{:width$}", state.cursor + 1, width = max_length);
prompt = format!("{}›", cursor_padded);
}
}
#[cfg(feature = "repl")]
fn interpret_command(command_args: Vec<&str>, state: &mut EditorState) -> ExitStatus {
let output = match command_args[0]
.to_lowercase()
.replace(state.prefix.as_str(), "")
.as_str()
{
"exit" | "quit" => return ExitStatus::Failure("exit".to_string()),
_ => {
let mut args = command_args.clone();
let prefix_cloned = state.prefix.clone();
if let Some(stripped) = args[0].to_lowercase().strip_prefix(prefix_cloned.as_str()) {
args[0] = stripped;
let cmd = run_repl_command(args, state);
if let ExitStatus::Success(msg) = cmd {
msg
} else {
format!("{}", cmd)
}
} else {
panic!("unable to process non-repl command");
}
}
};
ExitStatus::Success(output)
}