Просмотр исходного кода

drk: introduced interactive shell infra

skoupidi 1 год назад
Родитель
Сommit
603ebe74f8

+ 11 - 0
Cargo.lock

@@ -2468,6 +2468,7 @@ dependencies = [
  "darkfi_money_contract",
  "easy-parallel",
  "lazy_static",
+ "linenoise-rs",
  "log",
  "num-bigint",
  "prettytable-rs",
@@ -4098,6 +4099,16 @@ dependencies = [
  "url",
 ]
 
+[[package]]
+name = "linenoise-rs"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "51e207483a55cd856bc85af576f663b2ebd25616e7b624c4ba42cff9ec2f0631"
+dependencies = [
+ "lazy_static",
+ "libc",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.9.4"

+ 1 - 0
bin/drk/Cargo.toml

@@ -21,6 +21,7 @@ darkfi-serial = "0.5.0"
 blake3 = "1.8.2"
 bs58 = "0.5.1"
 lazy_static = "1.5.0"
+linenoise-rs = "0.1.1"
 log = "0.4.27"
 num-bigint = "0.4.6"
 prettytable-rs = "0.10.0"

+ 9 - 0
bin/drk/drk_config.toml

@@ -26,6 +26,9 @@ wallet_pass = "changeme"
 # darkfid JSON-RPC endpoint
 endpoint = "tcp://127.0.0.1:8240"
 
+# Path to interactive shell history file
+history_path = "~/.local/share/darkfi/drk/localnet/history.txt"
+
 # Testnet blockchain network configuration
 [network_config."testnet"]
 # Path to blockchain cache database
@@ -40,6 +43,9 @@ wallet_pass = "changeme"
 # darkfid JSON-RPC endpoint
 endpoint = "tcp://127.0.0.1:8340"
 
+# Path to interactive shell history file
+history_path = "~/.local/share/darkfi/drk/testnet/history.txt"
+
 # Mainnet blockchain network configuration
 [network_config."mainnet"]
 # Path to blockchain cache database
@@ -53,3 +59,6 @@ wallet_pass = "changeme"
 
 # darkfid JSON-RPC endpoint
 endpoint = "tcp://127.0.0.1:8440"
+
+# Path to interactive shell history file
+history_path = "~/.local/share/darkfi/drk/mainnet/history.txt"

+ 4 - 0
bin/drk/src/cli_util.rs

@@ -113,6 +113,9 @@ pub async fn kaching() {
 pub fn generate_completions(shell: &str) -> Result<()> {
     // Sub-commands
 
+    // Interactive
+    let interactive = SubCommand::with_name("interactive").about("Enter Drk interactive shell");
+
     // Kaching
     let kaching = SubCommand::with_name("kaching").about("Fun");
 
@@ -504,6 +507,7 @@ pub fn generate_completions(shell: &str) -> Result<()> {
         .help("Blockchain network to use");
 
     let command = vec![
+        interactive,
         kaching,
         ping,
         completions,

+ 152 - 0
bin/drk/src/interactive.rs

@@ -0,0 +1,152 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+use linenoise_rs::{
+    linenoise, linenoise_history_add, linenoise_history_load, linenoise_history_save,
+    linenoise_set_completion_callback, linenoise_set_hints_callback,
+};
+
+use darkfi::{system::StoppableTask, util::path::expand_path};
+
+use crate::{
+    cli_util::{generate_completions, kaching},
+    Drk,
+};
+
+// TODO:
+//  1. add rest commands handling, along with their completions and hints.
+//  2. add input definitions, so you input from files not just stdin.
+//  3. add output definitions, so you can output to files not just stdout.
+//  4. create a transactions cache in the wallet db, so you can use it to handle them.
+
+/// Auxiliary function to define the interactive shell completions.
+fn completion(buf: &str, lc: &mut Vec<String>) {
+    if buf.starts_with("h") {
+        lc.push("help".to_string());
+        return
+    }
+
+    if buf.starts_with("k") {
+        lc.push("kaching".to_string());
+        return
+    }
+
+    if buf.starts_with("p") {
+        lc.push("ping".to_string());
+        return
+    }
+
+    if buf.starts_with("c") {
+        lc.push("completions".to_string());
+    }
+}
+
+/// Auxiliary function to define the interactive shell hints.
+fn hints(buf: &str) -> Option<(String, i32, bool)> {
+    match buf {
+        "completions " => Some(("{shell}".to_string(), 35, false)), // 35 = magenta
+        _ => None,
+    }
+}
+
+/// Auxiliary function to start provided Drk as an interactive shell.
+pub async fn interactive(drk: &Drk, history_path: &str) {
+    // Expand the history file path
+    let history_path = match expand_path(history_path) {
+        Ok(p) => p,
+        Err(e) => {
+            println!("Error while expanding history file path: {e}");
+            return
+        }
+    };
+    let history_path = history_path.into_os_string();
+    let history_file = history_path.to_str().unwrap();
+
+    // Set the completion callback. This will be called every time the
+    // user uses the <tab> key.
+    linenoise_set_completion_callback(completion);
+
+    // Set the shell hints
+    linenoise_set_hints_callback(hints);
+
+    // Load history from file.The history file is just a plain text file
+    // where entries are separated by newlines.
+    let _ = linenoise_history_load(history_file);
+
+    // Create a detached task to use for block subscription
+    let subscription_active = false;
+    let subscription_task = StoppableTask::new();
+
+    // Start the interactive shell
+    loop {
+        // Grab input or end if Ctrl-D or Ctrl-C was pressed
+        let Some(line) = linenoise("drk> ") else {
+            // Stop the subscription task if its active
+            if subscription_active {
+                subscription_task.stop().await;
+            }
+
+            // Write history file
+            let _ = linenoise_history_save(history_file);
+
+            return
+        };
+
+        // Check if line is empty
+        if line.is_empty() {
+            continue
+        }
+
+        // Add line to history
+        linenoise_history_add(&line);
+
+        // Parse command parts
+        let parts: Vec<&str> = line.split_whitespace().collect();
+        if parts.is_empty() {
+            continue
+        }
+
+        // Handle command
+        match parts[0] {
+            "help" => println!("hhhheeeelp"),
+            "kaching" => kaching().await,
+            "ping" => handle_ping(drk).await,
+            "completions" => handle_completions(&parts),
+            _ => println!("Unreconized command: {}", parts[0]),
+        }
+    }
+}
+
+/// Auxiliary function to define the ping command handling.
+async fn handle_ping(drk: &Drk) {
+    if let Err(e) = drk.ping().await {
+        println!("Error while executing ping command: {e}")
+    }
+}
+
+/// Auxiliary function to define the completions command handling.
+fn handle_completions(parts: &[&str]) {
+    if parts.len() != 2 {
+        println!("Malformed `completions` command");
+        println!("Usage: completions {{shell}}");
+    }
+
+    if let Err(e) = generate_completions(parts[1]) {
+        println!("Error while executing completions command: {e}")
+    }
+}

+ 3 - 0
bin/drk/src/lib.rs

@@ -41,6 +41,9 @@ pub mod token;
 /// CLI utility functions
 pub mod cli_util;
 
+/// Drk interactive shell
+pub mod interactive;
+
 /// Wallet functionality related to Money
 pub mod money;
 

+ 22 - 0
bin/drk/src/main.rs

@@ -57,6 +57,7 @@ use drk::{
         generate_completions, kaching, parse_token_pair, parse_tx_from_stdin, parse_value_pair,
     },
     dao::{DaoParams, ProposalRecord},
+    interactive::interactive,
     money::BALANCE_BASE10_DECIMALS,
     swap::PartialSwapData,
     Drk,
@@ -100,6 +101,9 @@ struct Args {
 // don't forget to update cli_util::generate_completions()
 #[derive(Clone, Debug, Deserialize, StructOpt)]
 enum Subcmd {
+    /// Enter Drk interactive shell
+    Interactive,
+
     /// Fun
     Kaching,
 
@@ -565,6 +569,10 @@ struct BlockchainNetwork {
     #[structopt(short, long, default_value = "tcp://127.0.0.1:8240")]
     /// darkfid JSON-RPC endpoint
     endpoint: Url,
+
+    #[structopt(long, default_value = "~/.local/share/darkfi/drk/localnet/history.txt")]
+    /// Path to interactive shell history file
+    history_path: String,
 }
 
 /// Auxiliary function to parse darkfid configuration file and extract requested
@@ -648,6 +656,20 @@ async fn realmain(args: Args, ex: Arc<smol::Executor<'static>>) -> Result<()> {
     };
 
     match args.command {
+        Subcmd::Interactive => {
+            let drk = new_wallet(
+                blockchain_config.cache_path,
+                blockchain_config.wallet_path,
+                blockchain_config.wallet_pass,
+                Some(blockchain_config.endpoint),
+                ex,
+                args.fun,
+            )
+            .await;
+            interactive(&drk, &blockchain_config.history_path).await;
+            drk.stop_rpc_client().await
+        }
+
         Subcmd::Kaching => {
             if !args.fun {
                 println!("Apparently you don't like fun...");

+ 3 - 0
contrib/localnet/darkfid-single-node/drk.toml

@@ -22,3 +22,6 @@ wallet_pass = "testing"
 
 # darkfid JSON-RPC endpoint
 endpoint = "tcp://127.0.0.1:48240"
+
+# Path to interactive shell history file
+history_path = "drk/history.txt"