novel_cli/cmd/
mod.rs

1pub mod bookshelf;
2pub mod build;
3pub mod check;
4pub mod completions;
5pub mod download;
6pub mod info;
7pub mod read;
8pub mod real_cugan;
9pub mod search;
10pub mod sign;
11pub mod template;
12pub mod transform;
13pub mod unzip;
14pub mod update;
15pub mod zip;
16
17use std::collections::HashMap;
18use std::path::Path;
19use std::sync::Arc;
20use std::{cmp, process};
21
22use clap::ValueEnum;
23use fluent_templates::Loader;
24use fluent_templates::fluent_bundle::FluentValue;
25use novel_api::Client;
26use ratatui::buffer::Buffer;
27use ratatui::layout::{Rect, Size};
28use ratatui::text::Text;
29use ratatui::widgets::block::Title;
30use ratatui::widgets::{Block, Paragraph, StatefulWidget, Widget, Wrap};
31use strum::AsRefStr;
32use tokio::signal;
33use tui_widgets::scrollview::{ScrollView, ScrollViewState};
34use url::Url;
35
36use crate::{LANG_ID, LOCALES};
37
38const DEFAULT_PROXY: &str = "http://127.0.0.1:8080";
39
40const DEFAULT_PROXY_SURGE: &str = "http://127.0.0.1:6152";
41
42#[must_use]
43#[derive(Clone, PartialEq, ValueEnum, AsRefStr)]
44pub enum Source {
45    #[strum(serialize = "sfacg")]
46    Sfacg,
47    #[strum(serialize = "ciweimao")]
48    Ciweimao,
49    #[strum(serialize = "ciyuanji")]
50    Ciyuanji,
51}
52
53#[must_use]
54#[derive(Clone, PartialEq, ValueEnum, AsRefStr)]
55pub enum Format {
56    Pandoc,
57    Mdbook,
58}
59
60#[must_use]
61#[derive(Clone, PartialEq, ValueEnum, AsRefStr)]
62pub enum Convert {
63    S2T,
64    T2S,
65    JP2T2S,
66    CUSTOM,
67}
68
69#[inline]
70#[must_use]
71fn default_cert_path() -> String {
72    novel_api::home_dir_path()
73        .unwrap()
74        .join(".mitmproxy")
75        .join("mitmproxy-ca-cert.pem")
76        .display()
77        .to_string()
78}
79
80fn set_options<T, E>(client: &mut T, proxy: &Option<Url>, no_proxy: &bool, cert: &Option<E>)
81where
82    T: Client,
83    E: AsRef<Path>,
84{
85    if let Some(proxy) = proxy {
86        client.proxy(proxy.clone());
87    }
88
89    if *no_proxy {
90        client.no_proxy();
91    }
92
93    if let Some(cert) = cert {
94        client.cert(cert.as_ref().to_path_buf())
95    }
96}
97
98fn handle_ctrl_c<T>(client: &Arc<T>)
99where
100    T: Client + Send + Sync + 'static,
101{
102    let client = Arc::clone(client);
103
104    tokio::spawn(async move {
105        signal::ctrl_c().await.unwrap();
106
107        tracing::warn!("Download terminated, login data will be saved");
108
109        client.shutdown().await.unwrap();
110        process::exit(128 + libc::SIGINT);
111    });
112}
113
114fn cert_help_msg() -> String {
115    let args = {
116        let mut map = HashMap::new();
117        map.insert(
118            "cert_path".into(),
119            FluentValue::String(default_cert_path().into()),
120        );
121        map
122    };
123
124    LOCALES.lookup_with_args(&LANG_ID, "cert", &args)
125}
126
127#[derive(Default, PartialEq)]
128enum Mode {
129    #[default]
130    Running,
131    Quit,
132}
133
134pub struct ScrollableParagraph<'a> {
135    title: Option<Title<'a>>,
136    text: Text<'a>,
137}
138
139impl<'a> ScrollableParagraph<'a> {
140    pub fn new<T>(text: T) -> Self
141    where
142        T: Into<Text<'a>>,
143    {
144        ScrollableParagraph {
145            title: None,
146            text: text.into(),
147        }
148    }
149
150    pub fn title<T>(self, title: T) -> Self
151    where
152        T: Into<Title<'a>>,
153    {
154        ScrollableParagraph {
155            title: Some(title.into()),
156            ..self
157        }
158    }
159}
160
161impl StatefulWidget for ScrollableParagraph<'_> {
162    type State = ScrollViewState;
163
164    fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
165        let mut block = Block::bordered();
166        if self.title.is_some() {
167            block = block.title(self.title.as_ref().unwrap().clone());
168        }
169
170        let paragraph = Paragraph::new(self.text.clone()).wrap(Wrap { trim: false });
171        let mut scroll_view = ScrollView::new(Size::new(area.width - 1, area.height));
172        let mut block_area = block.inner(scroll_view.buf().area);
173
174        let scroll_height = cmp::max(
175            paragraph.line_count(block_area.width) as u16
176                + (scroll_view.buf().area.height - block_area.height),
177            area.height,
178        );
179
180        let scroll_width = if area.height >= scroll_height {
181            // 不需要滚动条
182            area.width
183        } else {
184            area.width - 1
185        };
186
187        scroll_view = ScrollView::new(Size::new(scroll_width, scroll_height));
188
189        let scroll_view_buf = scroll_view.buf_mut();
190        block_area = block.inner(scroll_view_buf.area);
191
192        Widget::render(block, scroll_view_buf.area, scroll_view_buf);
193        Widget::render(paragraph, block_area, scroll_view_buf);
194        StatefulWidget::render(scroll_view, area, buf, state);
195    }
196}