Skip to content

Commit

Permalink
feat(actions): add edit action (#22)
Browse files Browse the repository at this point in the history
The new `edit` action. Default key binding is `e`.

With this, otree can open current item in system editor to let user better view or copy contents.

All the edited changes won't be saved. This is **readonly** to the original data.
  • Loading branch information
fioncat committed Jun 14, 2024
1 parent dda1c83 commit bbaf9d5
Show file tree
Hide file tree
Showing 14 changed files with 285 additions and 39 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ For how to configure TUI colors, please refer to: [Colors Document](docs/colors.
- [x] Action: Mouse click actions
- [x] Action: Mouse scroll actions
- [ ] Action: Mouse select actions
- [ ] Action: Open current selected item in editor
- [x] Action: Open current selected item in editor **ReadOnly** (v0.2)
- [x] Action: Switch between tree overview and data block (v0.1)
- [x] Action: Jump to parent item (v0.1)
- [x] Action: Jump to parent item and close (v0.1)
Expand Down
5 changes: 5 additions & 0 deletions config/default.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
[editor]
program = "vim"
args = ["{{file}}"]
dir = "/tmp"

[layout]
direction = "horizontal"
tree_size = 40
Expand Down
39 changes: 20 additions & 19 deletions docs/actions.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,26 @@
# All Available Actions

| Action | Default Keys | Description |
| --------------- | ------------------------- | ------------------------------------------------------------- |
| move_up | `k`, `<up>` | Move cursor up |
| move_down | `j`, `<down>` | Move cursor down |
| move_left | `h`, `<left>` | Move cursor left |
| move_right | `l`, `<right>` | Move cursor right |
| select_focus | `<enter>` | Toggle select current item |
| select_parent | `p` | Move cursor to the parent item |
| select_first | `g` | Move cursor to the top |
| select_last | `G` | Move cursor to the bottom |
| close_parent | `<backspace>` | Move cursor to the parent and close |
| Action | Default Keys | Description |
| --------------- | ------------------------- | ------------------------------------------------------------ |
| move_up | `k`, `<up>` | Move cursor up |
| move_down | `j`, `<down>` | Move cursor down |
| move_left | `h`, `<left>` | Move cursor left |
| move_right | `l`, `<right>` | Move cursor right |
| select_focus | `<enter>` | Toggle select current item |
| select_parent | `p` | Move cursor to the parent item |
| select_first | `g` | Move cursor to the top |
| select_last | `G` | Move cursor to the bottom |
| close_parent | `<backspace>` | Move cursor to the parent and close |
| change_root | `r` | Change current item as root<br/>Use `reset` action to recover |
| reset | `<esc>` | Reset cursor and items |
| page_up | `<page-up>`, `<ctrl-y>` | Scroll up |
| page_down | `<page-down>`, `<ctrl-e>` | Scroll down |
| change_layout | `v` | Change current layout |
| tree_scale_up | `[` | Scale up tree widget |
| tree_scale_down | `]` | Scale down tree widget |
| switch | `<tab>` | Switch focus widget |
| quit | `<ctrl-c>`, `q` | Quit program |
| reset | `<esc>` | Reset cursor and items |
| page_up | `<page-up>`, `<ctrl-y>` | Scroll up |
| page_down | `<page-down>`, `<ctrl-e>` | Scroll down |
| change_layout | `v` | Change current layout |
| tree_scale_up | `[` | Scale up tree widget |
| tree_scale_down | `]` | Scale down tree widget |
| switch | `<tab>` | Switch focus widget |
| edit | `e` | Open current item in editor<br />**(ReadOnly)** |
| quit | `<ctrl-c>`, `q` | Quit program |

All available keys:

Expand Down
5 changes: 5 additions & 0 deletions src/config/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,9 @@ pub struct Keys {
#[serde(default = "Keys::default_switch")]
pub switch: Vec<String>,

#[serde(default = "Keys::default_edit")]
pub edit: Vec<String>,

#[serde(default = "Keys::default_quit")]
pub quit: Vec<String>,

Expand All @@ -265,6 +268,7 @@ generate_keys_default!(
tree_scale_up => ["["],
tree_scale_down => ["]"],
switch => ["<tab>"],
edit => ["e"],
quit => ["<ctrl-c>", "q"]
);

Expand All @@ -286,6 +290,7 @@ generate_actions!(
tree_scale_up => TreeScaleUp,
tree_scale_down => TreeScaleDown,
switch => Switch,
edit => Edit,
quit => Quit
);

Expand Down
41 changes: 41 additions & 0 deletions src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ use self::types::Types;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
#[serde(default = "Editor::default")]
pub editor: Editor,

#[serde(default = "Data::default")]
pub data: Data,

Expand All @@ -37,6 +40,18 @@ pub struct Config {
pub keys: Keys,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Editor {
#[serde(default = "Editor::default_program")]
pub program: String,

#[serde(default = "Editor::default_args")]
pub args: Vec<String>,

#[serde(default = "Editor::default_dir")]
pub dir: String,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Layout {
#[serde(default = "Layout::default_direction")]
Expand Down Expand Up @@ -140,6 +155,7 @@ impl Config {

pub fn default() -> Self {
Self {
editor: Editor::default(),
data: Data::default(),
layout: Layout::default(),
header: Header::default(),
Expand All @@ -165,6 +181,31 @@ impl Config {
}
}

impl Editor {
fn default() -> Self {
Self {
program: Self::default_program(),
args: Self::default_args(),
dir: Self::default_dir(),
}
}

fn default_program() -> String {
if let Some(editor) = env::var_os("EDITOR") {
return editor.to_string_lossy().to_string();
}
String::from("vim")
}

fn default_args() -> Vec<String> {
vec![String::from("{file}")]
}

fn default_dir() -> String {
String::from("/tmp")
}
}

impl Layout {
fn default() -> Self {
Self {
Expand Down
103 changes: 103 additions & 0 deletions src/edit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use std::fs;
use std::io::{self, Read, Write};
use std::path::PathBuf;
use std::process::{Command, Stdio};

use anyhow::{bail, Context, Result};

use crate::config::Config;

pub struct Edit {
path: String,
data: String,
cmd: Command,
}

impl Edit {
pub fn new(cfg: &Config, identify: String, data: String, extension: &'static str) -> Self {
let mut cmd = Command::new(&cfg.editor.program);
cmd.stdin(Stdio::inherit());
cmd.stdout(Stdio::inherit());
cmd.stderr(Stdio::inherit());

let name = identify.replace('/', "_");
let path = PathBuf::from(&cfg.editor.dir).join(format!("otree_{name}.{extension}"));
let path = format!("{}", path.display());

for arg in cfg.editor.args.iter() {
if !arg.contains("{file}") {
cmd.arg(arg);
continue;
}

let arg = arg.replace("{file}", &path);
cmd.arg(arg);
}

Self { path, data, cmd }
}

pub fn run(mut self) {
if let Err(err) = self._run() {
eprintln!("Edit error: {err:#}");
eprintln!();
eprintln!("Press any key to continue...");
io::stdout().flush().unwrap();

// Wait for a single character input
let mut buffer = [0; 1];
io::stdin().read_exact(&mut buffer).unwrap();
}
}

fn _run(&mut self) -> Result<()> {
self.write_file().context("write edit file")?;

let result = self.edit_file().context("edit file");
if result.is_err() {
let _ = self.delete_file();
return result;
}

self.delete_file().context("delete edit file")?;
Ok(())
}

fn write_file(&self) -> Result<()> {
let path = PathBuf::from(&self.path);
if let Some(dir) = path.parent() {
match fs::metadata(dir) {
Ok(meta) => {
if !meta.is_dir() {
bail!("'{}' is not a directory", dir.display());
}
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {
fs::create_dir_all(dir)
.with_context(|| format!("create directory '{}'", dir.display()))?;
}
Err(err) => {
return Err(err)
.with_context(|| format!("get metadata for '{}'", dir.display()))
}
}
}

fs::write(&path, &self.data)
.with_context(|| format!("write data to file '{}'", path.display()))?;

Ok(())
}

fn edit_file(&mut self) -> Result<()> {
let status = self.cmd.status().context("execute editor command")?;
if !status.success() {
bail!("editor command exited with bad code");
}
Ok(())
}

fn delete_file(&self) -> Result<()> {
fs::remove_file(&self.path).with_context(|| format!("delete file '{}'", self.path))
}
}
11 changes: 2 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
mod cmd;
mod config;
mod edit;
mod parse;
mod tree;
mod ui;
Expand Down Expand Up @@ -105,15 +106,7 @@ fn run() -> Result<()> {
app.set_header(header_ctx);
}

let mut terminal = ui::start().context("start tui")?;
let result = app.show(&mut terminal).context("show tui");

// Regardless of how the TUI app executes, we should always restore the terminal.
// Otherwise, if the app encounters an error (such as a draw error), the user's terminal
// will become a mess.
ui::restore(terminal).context("restore terminal")?;

result
ui::start(app)
}

fn main() {
Expand Down
4 changes: 4 additions & 0 deletions src/parse/json.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ use super::{Parser, SyntaxToken};
pub(super) struct JsonParser {}

impl Parser for JsonParser {
fn extension(&self) -> &'static str {
"json"
}

fn parse(&self, data: &str) -> Result<Value> {
serde_json::from_str(data).context("parse JSON")
}
Expand Down
2 changes: 2 additions & 0 deletions src/parse/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ pub enum ContentType {
}

pub trait Parser {
fn extension(&self) -> &'static str;

fn parse(&self, data: &str) -> Result<Value>;

fn to_string(&self, value: &Value) -> String;
Expand Down
4 changes: 4 additions & 0 deletions src/parse/toml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ use super::{Parser, SyntaxToken};
pub(super) struct TomlParser {}

impl Parser for TomlParser {
fn extension(&self) -> &'static str {
"toml"
}

fn parse(&self, data: &str) -> Result<Value> {
let toml_value: TomlValue = toml::from_str(data).context("parse TOML")?;
Ok(toml_value_to_json(toml_value))
Expand Down
4 changes: 4 additions & 0 deletions src/parse/yaml.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ use super::{Parser, SyntaxToken};
pub(super) struct YamlParser {}

impl Parser for YamlParser {
fn extension(&self) -> &'static str {
"yaml"
}

fn parse(&self, data: &str) -> Result<Value> {
let mut values = Vec::with_capacity(1);
for document in serde_yml::Deserializer::from_str(data) {
Expand Down
Loading

0 comments on commit bbaf9d5

Please sign in to comment.