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