chore(deps): 升级依赖并新增正则与位运算相关库
This commit is contained in:
924
Cargo.lock
generated
924
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -7,8 +7,8 @@ description = "A command-line tool to manage memos"
|
|||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
ratatui = "0.28"
|
ratatui = "0.30"
|
||||||
crossterm = "0.28"
|
crossterm = "0.29"
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
use crate::models::{CreateMemoRequest, CreateMemoResponse, ListMemosResponse, Memo};
|
use crate::models::{CreateMemoRequest, CreateMemoResponse, ListMemosResponse, Memo};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct MemosClient {
|
pub struct MemosClient {
|
||||||
|
|||||||
61
src/main.rs
61
src/main.rs
@@ -167,6 +167,67 @@ async fn run_tui_mode() -> Result<()> {
|
|||||||
match app.mode {
|
match app.mode {
|
||||||
AppMode::MainMenu => {
|
AppMode::MainMenu => {
|
||||||
match key.code {
|
match key.code {
|
||||||
|
KeyCode::Up => {
|
||||||
|
if let Some(selected) = app.list_state.selected() {
|
||||||
|
if selected > 0 {
|
||||||
|
app.list_state.select(Some(selected - 1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app.list_state.select(Some(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Down => {
|
||||||
|
if let Some(selected) = app.list_state.selected() {
|
||||||
|
if selected < 3 { // 4 items, 0-3 index
|
||||||
|
app.list_state.select(Some(selected + 1));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
app.list_state.select(Some(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
KeyCode::Enter => {
|
||||||
|
if let Some(selected) = app.list_state.selected() {
|
||||||
|
match selected {
|
||||||
|
0 => { // List Memos
|
||||||
|
if app.config.is_configured() {
|
||||||
|
app.loading = true;
|
||||||
|
terminal.draw(|f| tui::render_app(f, &mut app))?;
|
||||||
|
match load_memos(&app).await {
|
||||||
|
Ok(memos) => {
|
||||||
|
app.memos = memos;
|
||||||
|
app.mode = AppMode::ListMemos;
|
||||||
|
app.message = Some("Memos loaded successfully".to_string());
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
app.message = Some(format!("Error: {}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
app.loading = false;
|
||||||
|
} else {
|
||||||
|
app.message = Some("Please configure first".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1 => { // Create Memo
|
||||||
|
if app.config.is_configured() {
|
||||||
|
app.input_content.clear();
|
||||||
|
app.input_visibility = "PRIVATE".to_string();
|
||||||
|
app.mode = AppMode::CreateMemo;
|
||||||
|
} else {
|
||||||
|
app.message = Some("Please configure first".to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2 => { // Configuration
|
||||||
|
app.input_base_url = app.config.base_url.clone();
|
||||||
|
app.input_token = app.config.user_token.clone();
|
||||||
|
app.mode = AppMode::Config;
|
||||||
|
}
|
||||||
|
3 => { // Quit
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
KeyCode::Char('1') => {
|
KeyCode::Char('1') => {
|
||||||
if app.config.is_configured() {
|
if app.config.is_configured() {
|
||||||
app.loading = true;
|
app.loading = true;
|
||||||
|
|||||||
92
src/tui.rs
92
src/tui.rs
@@ -2,15 +2,14 @@ use crate::config::Config;
|
|||||||
use crate::client::MemosClient;
|
use crate::client::MemosClient;
|
||||||
use crate::models::Memo;
|
use crate::models::Memo;
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use crossterm::event::{Event, KeyCode, KeyEventKind};
|
|
||||||
use ratatui::backend::CrosstermBackend;
|
|
||||||
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
use ratatui::layout::{Alignment, Constraint, Direction, Layout, Rect};
|
||||||
use ratatui::style::{Color, Modifier, Style, Stylize};
|
use ratatui::style::{Color, Modifier, Style};
|
||||||
use ratatui::text::{Line, Span};
|
use ratatui::text::{Line, Span};
|
||||||
use ratatui::widgets::{Block, Borders, Clear, List, ListItem, Paragraph, Wrap};
|
use ratatui::widgets::{Block, Borders, Clear, List, ListDirection, ListItem, ListState, Paragraph};
|
||||||
use ratatui::Frame;
|
use ratatui::Frame;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
use chrono::Local;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum AppMode {
|
pub enum AppMode {
|
||||||
@@ -35,10 +34,13 @@ pub struct App {
|
|||||||
pub loading: bool,
|
pub loading: bool,
|
||||||
client: Arc<Mutex<Option<MemosClient>>>,
|
client: Arc<Mutex<Option<MemosClient>>>,
|
||||||
pub list_selected: Option<usize>,
|
pub list_selected: Option<usize>,
|
||||||
|
pub list_state: ListState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
pub fn new(config: Config) -> Self {
|
pub fn new(config: Config) -> Self {
|
||||||
|
let mut list_state = ListState::default();
|
||||||
|
list_state.select(Some(0));
|
||||||
Self {
|
Self {
|
||||||
mode: AppMode::MainMenu,
|
mode: AppMode::MainMenu,
|
||||||
config,
|
config,
|
||||||
@@ -53,6 +55,7 @@ impl App {
|
|||||||
loading: false,
|
loading: false,
|
||||||
client: Arc::new(Mutex::new(None)),
|
client: Arc::new(Mutex::new(None)),
|
||||||
list_selected: None,
|
list_selected: None,
|
||||||
|
list_state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +93,10 @@ pub fn render_app(frame: &mut Frame, app: &mut App) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Some(msg) = &app.message {
|
if let Some(msg) = &app.message {
|
||||||
let area = Rect::new(10, frame.area().height - 3, frame.area().width - 10, frame.area().height - 1);
|
let y = frame.area().height.saturating_sub(3);
|
||||||
|
let height = 2;
|
||||||
|
if y + height <= frame.area().height {
|
||||||
|
let area = Rect::new(10, y, frame.area().width - 20, height);
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.style(Style::default().fg(Color::Yellow));
|
.style(Style::default().fg(Color::Yellow));
|
||||||
@@ -100,14 +106,15 @@ pub fn render_app(frame: &mut Frame, app: &mut App) {
|
|||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
frame.render_widget(text, area);
|
frame.render_widget(text, area);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if app.loading {
|
if app.loading {
|
||||||
let area = Rect::new(
|
let width = 20;
|
||||||
frame.area().width / 2 - 10,
|
let height = 6;
|
||||||
frame.area().height / 2 - 3,
|
let x = (frame.area().width / 2).saturating_sub(width / 2);
|
||||||
frame.area().width / 2 + 10,
|
let y = (frame.area().height / 2).saturating_sub(height / 2);
|
||||||
frame.area().height / 2 + 3,
|
if x + width <= frame.area().width && y + height <= frame.area().height {
|
||||||
);
|
let area = Rect::new(x, y, width, height);
|
||||||
let block = Block::default()
|
let block = Block::default()
|
||||||
.borders(Borders::ALL)
|
.borders(Borders::ALL)
|
||||||
.title("Loading...")
|
.title("Loading...")
|
||||||
@@ -116,23 +123,54 @@ pub fn render_app(frame: &mut Frame, app: &mut App) {
|
|||||||
frame.render_widget(block, area);
|
frame.render_widget(block, area);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn render_main_menu(frame: &mut Frame, app: &App) {
|
fn render_main_menu(frame: &mut Frame, app: &mut App) {
|
||||||
let chunks = Layout::default()
|
let chunks = Layout::default()
|
||||||
.direction(Direction::Vertical)
|
.direction(Direction::Vertical)
|
||||||
.constraints([
|
.constraints([
|
||||||
Constraint::Length(3),
|
Constraint::Length(6), // 艺术字体标题
|
||||||
Constraint::Min(0),
|
Constraint::Length(1), // 产品版本
|
||||||
Constraint::Length(3),
|
Constraint::Length(1), // 作者及联系方式
|
||||||
|
Constraint::Length(1), // 数字时钟
|
||||||
|
Constraint::Min(0), // 功能选择
|
||||||
|
Constraint::Length(2), // 特殊提示
|
||||||
])
|
])
|
||||||
.split(frame.area());
|
.split(frame.area());
|
||||||
|
|
||||||
let title = Paragraph::new("Memos CLI")
|
// 艺术字体标题 (Memos CLI)
|
||||||
|
let art_title = vec![
|
||||||
|
Line::from("███╗ ███╗███████╗███╗ ███╗ ██████╗ ███████╗".to_string()),
|
||||||
|
Line::from("████╗ ████║██╔════╝████╗ ████║██╔═══██╗██╔════╝".to_string()),
|
||||||
|
Line::from("██╔████╔██║█████╗ ██╔████╔██║██║ ██║███████╗".to_string()),
|
||||||
|
Line::from("██║╚██╔╝██║██╔══╝ ██║╚██╔╝██║██║ ██║╚════██║".to_string()),
|
||||||
|
Line::from("██║ ╚═╝ ██║███████╗██║ ╚═╝ ██║╚██████╔╝███████║".to_string()),
|
||||||
|
Line::from("╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝".to_string()),
|
||||||
|
];
|
||||||
|
let title = Paragraph::new(art_title)
|
||||||
.style(Style::default().fg(Color::Cyan).bold())
|
.style(Style::default().fg(Color::Cyan).bold())
|
||||||
.alignment(Alignment::Center)
|
.alignment(Alignment::Center);
|
||||||
.block(Block::default().borders(Borders::ALL).title("Welcome"));
|
|
||||||
frame.render_widget(title, chunks[0]);
|
frame.render_widget(title, chunks[0]);
|
||||||
|
|
||||||
|
// 产品版本
|
||||||
|
let version = Paragraph::new("Version: 1.0.0")
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
frame.render_widget(version, chunks[1]);
|
||||||
|
|
||||||
|
// 作者及联系方式
|
||||||
|
let author = Paragraph::new("Author: Your Name <your@email.com>")
|
||||||
|
.style(Style::default().fg(Color::White))
|
||||||
|
.alignment(Alignment::Center);
|
||||||
|
frame.render_widget(author, chunks[2]);
|
||||||
|
|
||||||
|
// 数字时钟
|
||||||
|
let current_time = Local::now().format("%Y-%m-%d %H:%M:%S").to_string();
|
||||||
|
let clock = Paragraph::new(format!("🕒 {}", current_time))
|
||||||
|
.style(Style::default().fg(Color::Yellow));
|
||||||
|
frame.render_widget(clock, chunks[3]);
|
||||||
|
|
||||||
|
// 功能选择
|
||||||
let items = vec![
|
let items = vec![
|
||||||
("1", "List Memos"),
|
("1", "List Memos"),
|
||||||
("2", "Create Memo"),
|
("2", "Create Memo"),
|
||||||
@@ -155,18 +193,16 @@ fn render_main_menu(frame: &mut Frame, app: &App) {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let menu = List::new(menu_items)
|
let menu = List::new(menu_items)
|
||||||
.block(Block::default().borders(Borders::ALL).title("Menu"));
|
.block(Block::default().borders(Borders::ALL).title("Menu"))
|
||||||
frame.render_widget(menu, chunks[1]);
|
.highlight_style(Style::default().bg(Color::DarkGray).fg(Color::Cyan).bold())
|
||||||
|
.direction(ListDirection::TopToBottom);
|
||||||
|
frame.render_stateful_widget(menu, chunks[4], &mut app.list_state);
|
||||||
|
|
||||||
let status = if app.config.is_configured() {
|
// 特殊提示
|
||||||
format!("Connected to: {}", app.config.base_url)
|
let hint = Paragraph::new("↑↓: Navigate | Enter: Select | Q: Quit")
|
||||||
} else {
|
.style(Style::default().fg(Color::DarkGray))
|
||||||
"Not configured".to_string()
|
|
||||||
};
|
|
||||||
let status_bar = Paragraph::new(status)
|
|
||||||
.style(Style::default().fg(Color::Green))
|
|
||||||
.alignment(Alignment::Center);
|
.alignment(Alignment::Center);
|
||||||
frame.render_widget(status_bar, chunks[2]);
|
frame.render_widget(hint, chunks[5]);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_config(frame: &mut Frame, app: &mut App) {
|
fn render_config(frame: &mut Frame, app: &mut App) {
|
||||||
|
|||||||
Reference in New Issue
Block a user