1
use exitfailure::ExitFailure;
2
use log::LevelFilter;
3
use ratatui::Terminal;
4
use server::config::set_config;
5
use server::start;
6
use sqlx::types::Uuid;
7
use std::env;
8
use std::str::FromStr;
9
use structopt::StructOpt;
10
use tokio::runtime::Handle;
11

            
12
use crossterm::event::{DisableMouseCapture, EnableMouseCapture};
13
use crossterm::{
14
    execute,
15
    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
16
};
17
use ratatui::backend::CrosstermBackend;
18
use std::sync::mpsc as std_mpsc;
19
use std::{io, io::Write};
20
use tokio::sync::mpsc;
21

            
22
mod run;
23
mod ui;
24

            
25
use run::{
26
    CliAccountBalance, CliAccountCreate, CliAccountList, CliCommand, CliCommodityCreate,
27
    CliCommodityList, CliGetConfig, CliSelectColumn, CliSetConfig, CliTransactionCreate,
28
    CliTransactionList, CliVersion, CommandNode,
29
};
30
use ui::{App, run_app};
31

            
32
#[derive(Debug)]
33
struct FieldContentPair {
34
    field: String,
35
    content: String,
36
}
37

            
38
impl FromStr for FieldContentPair {
39
    type Err = String;
40

            
41
    fn from_str(s: &str) -> Result<Self, Self::Err> {
42
        let parts: Vec<&str> = s.splitn(2, '=').collect();
43
        if parts.len() == 2 {
44
            Ok(FieldContentPair {
45
                field: parts[0].to_string(),
46
                content: parts[1].to_string(),
47
            })
48
        } else {
49
            Err("Expected format `field=content`".to_string())
50
        }
51
    }
52
}
53

            
54
// First, create a bridge between sync and async channels
55
struct LogWriter {
56
    sender: std_mpsc::Sender<String>,
57
}
58

            
59
impl Write for LogWriter {
60
    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
61
        if let Ok(s) = String::from_utf8(buf.to_vec())
62
            && let Err(e) = self.sender.send(s)
63
        {
64
            eprintln!("Failed to send to std channel: {e}");
65
        }
66
        Ok(buf.len())
67
    }
68

            
69
    fn flush(&mut self) -> std::io::Result<()> {
70
        Ok(())
71
    }
72
}
73

            
74
#[derive(StructOpt)]
75
struct Cli {
76
    #[structopt(short, long, parse(try_from_str))]
77
    userid: Uuid,
78

            
79
    #[structopt(short, long)]
80
    database: Option<String>,
81

            
82
    #[structopt(long, parse(try_from_str))]
83
    setopt: Option<FieldContentPair>,
84

            
85
    /// Account type
86
    #[structopt(long, parse(try_from_str), default_value = "Debug")]
87
    loglevel: LevelFilter,
88
}
89

            
90
#[tokio::main]
91
async fn main() -> Result<(), ExitFailure> {
92
    let args = Cli::from_args();
93

            
94
    // Create both sync and async channels
95
    let (std_tx, std_rx) = std_mpsc::channel::<String>();
96
    let (tx, rx) = mpsc::channel::<String>(1024);
97

            
98
    let _bridge_handle = tokio::spawn({
99
        let tx = tx.clone();
100
        async move {
101
            loop {
102
                match std_rx.recv() {
103
                    Ok(msg) => match tx.send(msg.clone()).await {
104
                        Ok(()) => {}
105
                        Err(e) => {
106
                            eprintln!("Bridge send error: {e}");
107
                            break;
108
                        }
109
                    },
110
                    Err(e) => {
111
                        eprintln!("Bridge receive error: {e}");
112
                        break;
113
                    }
114
                }
115
            }
116
            eprintln!("Bridge task ended");
117
        }
118
    });
119

            
120
    log::info!("Started");
121

            
122
    env_logger::Builder::new()
123
        .filter_level(args.loglevel)
124
        .target(env_logger::Target::Pipe(Box::new(LogWriter {
125
            sender: std_tx,
126
        })))
127
        .init();
128

            
129
    if let Some(db) = args.database {
130
        unsafe {
131
            env::set_var("DATABASE_URL", db);
132
        }
133
    }
134

            
135
    Handle::current()
136
        .spawn(start().await)
137
        .await
138
        .unwrap()
139
        .unwrap()
140
        .unwrap();
141

            
142
    if let Some(option) = args.setopt {
143
        set_config(&option.field, option.content.into()).await?;
144
    }
145

            
146
    // Terminal setup
147
    enable_raw_mode()?;
148
    let mut stdout = io::stdout();
149
    execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
150
    let backend = CrosstermBackend::new(stdout);
151
    let mut terminal = Terminal::new(backend)?;
152

            
153
    // Create demo command tree
154
    let commands = vec![
155
        CliVersion::node(),
156
        CommandNode {
157
            name: "transaction".to_string(),
158
            command: None,
159
            comment: "Access to transactions".to_string(),
160
            subcommands: vec![CliTransactionList::node(), CliTransactionCreate::node()],
161
            arguments: vec![],
162
        },
163
        CommandNode {
164
            name: "account".to_string(),
165
            command: None,
166
            comment: "Access to accounts".to_string(),
167
            subcommands: vec![
168
                CliAccountList::node(),
169
                CliAccountBalance::node(),
170
                CliAccountCreate::node(),
171
            ],
172
            arguments: vec![],
173
        },
174
        CommandNode {
175
            name: "commodity".to_string(),
176
            command: None,
177
            comment: "Access to commodities".to_string(),
178
            subcommands: vec![CliCommodityList::node(), CliCommodityCreate::node()],
179
            arguments: vec![],
180
        },
181
        CommandNode {
182
            name: "config".to_string(),
183
            command: None,
184
            comment: "Access to configuration".to_string(),
185
            subcommands: vec![CliGetConfig::node(), CliSetConfig::node()],
186
            arguments: vec![],
187
        },
188
        CommandNode {
189
            name: "sql".to_string(),
190
            command: None,
191
            comment: "Access to SQL database".to_string(),
192
            subcommands: vec![CliSelectColumn::node()],
193
            arguments: vec![],
194
        },
195
    ];
196

            
197
    let mut app = App::new(rx, args.userid);
198
    let res = run_app(&mut terminal, &mut app, &commands).await;
199

            
200
    // Cleanup
201
    disable_raw_mode()?;
202
    execute!(
203
        terminal.backend_mut(),
204
        LeaveAlternateScreen,
205
        DisableMouseCapture
206
    )?;
207
    terminal.show_cursor()?;
208

            
209
    if let Err(err) = res {
210
        println!("Error: {err:?}");
211
    }
212

            
213
    Ok(())
214
}