From 4292fafbfb70896d6e3aeb0f34b0c6113e2a4164 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=99=A7=E6=9A=90?= Date: Fri, 18 Nov 2022 14:47:01 +0800 Subject: [PATCH] Add text search (#4) * feat: Add detail mode * feat: Add focus mode and unfocus mode color style * feat: Add default color theme * feat: Add default color theme * feat: Add more description to README.md * feat: add .vscodecounter to .gitignore * feat: Use utf-8 decode title instead * feat: block detail mode if not selected any item * chore: remove unused import and rename internal variables * feat: Add search bar action * chore: remove unused import * feat: Add macors for sql generation * feat: Add search with plain text * feat: Add Searching feature --- .gitignore | 3 +- Cargo.toml | 1 + README.md | 29 ++++ src/application/app.rs | 121 ++++++++++++++-- src/application/error.rs | 1 + src/application/manage.rs | 10 +- src/application/stateful_list.rs | 36 ++++- src/common/system.rs | 2 +- src/domain/entity/tag.rs | 2 + src/domain/entity/workspace.rs | 45 +++--- src/domain/mod.rs | 1 + src/domain/repository/base.rs | 2 +- src/domain/searching/mod.rs | 1 + src/domain/searching/parse.rs | 137 ++++++++++++++++++ src/domain/system/error.rs | 1 + src/domain/system/init.rs | 2 +- src/domain/system/scan.rs | 9 +- src/infrastructure/repository/error.rs | 2 +- src/infrastructure/repository/macros.rs | 33 +++++ src/infrastructure/repository/mod.rs | 9 +- src/infrastructure/repository/schema.sql | 4 +- .../repository/workspace_repository.rs | 42 ++++-- src/presentation/management.rs | 68 ++++++++- src/presentation/mod.rs | 1 + src/presentation/style.rs | 25 ++++ src/presentation/text.rs | 10 +- src/presentation/ui.rs | 13 +- 27 files changed, 523 insertions(+), 87 deletions(-) create mode 100644 src/domain/searching/mod.rs create mode 100644 src/domain/searching/parse.rs create mode 100644 src/infrastructure/repository/macros.rs create mode 100644 src/presentation/style.rs diff --git a/.gitignore b/.gitignore index c1cd81b..1ce3f68 100644 --- a/.gitignore +++ b/.gitignore @@ -17,4 +17,5 @@ Cargo.lock # Developer .vscode/** -.vscode \ No newline at end of file +.vscode +.VSCodeCounter \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 8fe7964..3a964c1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,7 @@ crossbeam-channel = "0.5.6" thiserror = "1.0.37" rusqlite = { version = "0.28.0", features = ["bundled"] } unicode-width = "0.1.10" +regex = "1.7.0" [profile.release] diff --git a/README.md b/README.md index 63068cd..1fd26a3 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,17 @@ This is an ultimate unify vscode entry point. ## What is a workspaces organizer? +After months of or even years of hard working, have you notice that there are tons of folder which are open by vscode before? +You only want to find out a small experimental project in the workspaces history list but it hard to find because of the numbers of list. + +**Ruscode** is a best soluation for you! + +You can give your workspace tags to help you manage your workspaces. +You can search your workspaces by path, by folder name, or by tags which you gave before. +You can use terminal-UI application with beautiful color theme without hurting your eyes. + +Awesome! + ## Table of contents - [📦 Install](#install) @@ -28,6 +39,20 @@ This is an ultimate unify vscode entry point. - [✨ Creator](#creator) - [🌈 Contributors](#contributors) +## 🏹 Usage + +There are two mode in management page: ++ Search Mode ++ Detail Mode + +You can use arrow key to change between these two modes. +Also, you can find more detail of help text in the middle of screen. + + +### Search Mode + +### Detail Mode + ## Licence MIT @@ -37,3 +62,7 @@ MIT ## Contributors + + +## 🌟 Star History +[![Star History Chart](https://api.star-history.com/svg?repos=MissterHao/ruscode&type=Date)](https://star-history.com/#MissterHao/ruscode&Date) diff --git a/src/application/app.rs b/src/application/app.rs index 6a0c19d..3bda792 100644 --- a/src/application/app.rs +++ b/src/application/app.rs @@ -1,9 +1,10 @@ -use std::fmt; +use std::{fmt, vec}; use crate::{ common::system::SystemPaths, domain::{ entity::workspace::Workspace, + searching::parse::SearchingStrategy, system::{init::init_application_folders, scan::scan_workspaces_path}, }, infrastructure::repository::{ @@ -32,13 +33,27 @@ impl fmt::Display for ApplicationStatus { } } +#[derive(Clone, Copy)] +pub enum ApplicationControlMode { + SearchMode, + DetailMode, +} + +impl Default for ApplicationControlMode { + fn default() -> Self { + Self::SearchMode + } +} + pub struct App<'a> { pub title: &'a str, pub tabs: TabsState<'a>, pub status: ApplicationStatus, + pub control_mode: ApplicationControlMode, pub show_splash_screen: bool, pub workspaces: StatefulList, pub search_text: String, + workspaces_source: Vec, } impl<'a> App<'a> { @@ -47,9 +62,11 @@ impl<'a> App<'a> { title, tabs: TabsState::new(vec!["Workspaces", "Settings"]), status: ApplicationStatus::PrepareEnvironment, + control_mode: ApplicationControlMode::default(), show_splash_screen: show_splash_screen, workspaces: StatefulList::with_items(vec![]), search_text: String::new(), + workspaces_source: vec![], } } @@ -61,29 +78,109 @@ impl<'a> App<'a> { self.tabs.next(); } - pub fn on_up(&mut self) { - self.workspaces.previous(); + pub fn enter_detail_mode(&mut self) { + if !self.workspaces.has_selected_item() { + return; + }; + + match self.control_mode { + ApplicationControlMode::SearchMode => { + self.control_mode = ApplicationControlMode::DetailMode; + } + _ => {} + } } - pub fn on_down(&mut self) { - self.workspaces.next(); + pub fn enter_search_mode(&mut self) { + match self.control_mode { + ApplicationControlMode::DetailMode => { + self.control_mode = ApplicationControlMode::SearchMode; + } + _ => {} + } } - pub fn enter_in_workspace(&mut self) {} + pub fn on_up_list(&mut self) { + match self.control_mode { + ApplicationControlMode::SearchMode => { + self.workspaces.previous(); + } + ApplicationControlMode::DetailMode => {} + } + } + pub fn on_down_list(&mut self) { + match self.control_mode { + ApplicationControlMode::SearchMode => { + self.workspaces.next(); + } + ApplicationControlMode::DetailMode => {} + } + } + + pub fn on_enter(&mut self) { + match self.control_mode { + ApplicationControlMode::SearchMode => self.open_workspace(), + ApplicationControlMode::DetailMode => self.enter_new_tag(), + } + } + + pub fn on_backspace(&mut self) { + match self.control_mode { + ApplicationControlMode::SearchMode => { + self.search_text.pop(); + self.workspaces + .change_item_source(self.filtered_workspaces()); + } + ApplicationControlMode::DetailMode => { + todo!() + } + } + } + + pub fn filtered_workspaces(&self) -> Vec { + let strategy: SearchingStrategy = self.search_text.clone().into(); + + match strategy.searching_type { + crate::domain::searching::parse::SearchingStrategyType::All => { + self.workspaces_source.clone() + } + crate::domain::searching::parse::SearchingStrategyType::Tags => todo!(), + crate::domain::searching::parse::SearchingStrategyType::PlainText => self + .workspaces_source + .clone() + .iter() + .filter(|x| x.path.contains(&self.search_text)) + .map(|x| x.clone()) + .collect(), + crate::domain::searching::parse::SearchingStrategyType::PlainTextMixTags => todo!(), + } + } + + /// Open workspace by vscode + fn open_workspace(&mut self) {} + + /// Enter new tag for selected Workspace + fn enter_new_tag(&mut self) {} + + // pub fn on_key(&mut self, c: char) { - match c { - 't' => {} - _ => {} + match self.control_mode { + ApplicationControlMode::SearchMode => self.on_input_search_text(c), + ApplicationControlMode::DetailMode => todo!(), } } - pub fn on_tick(&mut self) {} + fn on_input_search_text(&mut self, c: char) { + self.search_text.push(c); + } + /// Scan all workspace record generated by vscode pub fn scan_workspaces(&mut self) -> Result, ApplicationError> { Ok(scan_workspaces_path()) } + /// Create database if not exist fn create_database(&self, path: &str) -> Result<(), DatabaseError> { create_database(path)?; Ok(()) @@ -105,10 +202,12 @@ impl<'a> App<'a> { let ret = WorkspaceRepository::sync_to_database(&workspaces) .expect("Syncing workspaces data failed."); - self.workspaces.items = ret; + self.workspaces_source = ret; + Ok(()) } + /// Manage all ApplicationStatus transition pub fn state_change(&mut self, next_state: ApplicationStatus) { match (self.status, next_state) { // Starts from SyncData diff --git a/src/application/error.rs b/src/application/error.rs index 4ef71fb..0ff23ee 100644 --- a/src/application/error.rs +++ b/src/application/error.rs @@ -1,6 +1,7 @@ use thiserror::Error; #[derive(Error, Debug)] +#[allow(dead_code)] pub enum ApplicationError { #[error("Database file can't create at `{0}`")] InitializeDatabaseFailed(String), diff --git a/src/application/manage.rs b/src/application/manage.rs index 5e5f316..80cbd2e 100644 --- a/src/application/manage.rs +++ b/src/application/manage.rs @@ -8,7 +8,6 @@ use crossterm::{ use std::{ error::Error, io, - sync::{Arc, Mutex}, time::{Duration, Instant}, }; use tui::{ @@ -60,10 +59,13 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( match key.code { KeyCode::Esc => app.on_escape_application(), KeyCode::Tab => app.next_tab(), - KeyCode::Up => app.on_up(), - KeyCode::Down => app.on_down(), + KeyCode::Up => app.on_up_list(), + KeyCode::Down => app.on_down_list(), + KeyCode::Right => app.enter_detail_mode(), + KeyCode::Left => app.enter_search_mode(), KeyCode::Char(c) => app.on_key(c), - KeyCode::Enter => app.enter_in_workspace(), + KeyCode::Enter => app.on_enter(), + KeyCode::Backspace => app.on_backspace(), _ => {} } } diff --git a/src/application/stateful_list.rs b/src/application/stateful_list.rs index ea95ad5..69ddb69 100644 --- a/src/application/stateful_list.rs +++ b/src/application/stateful_list.rs @@ -20,10 +20,34 @@ where } } + pub fn change_item_source(&mut self, items: Vec) { + self.items = items; + match self.state.selected() { + Some(curr) => { + if curr > self.items.len() { + self.state.select(Some(0)); + } + } + None => self.state.select(None), + } + } + + pub fn has_selected_item(&self) -> bool { + match self.state.selected() { + Some(_) => true, + None => false, + } + } + + #[allow(dead_code)] + pub fn unselected(&mut self) { + self.state.select(None); + } + pub fn next(&mut self) { - // if self.items.len() <= 0 { - // return; - // } + if self.items.len() <= 0 { + return; + } let i = match self.state.selected() { Some(i) => { @@ -39,9 +63,9 @@ where } pub fn previous(&mut self) { - // if self.items.len() <= 0 { - // return; - // } + if self.items.len() <= 0 { + return; + } let i = match self.state.selected() { Some(i) => { if i == 0 { diff --git a/src/common/system.rs b/src/common/system.rs index fc9532d..68e72ef 100644 --- a/src/common/system.rs +++ b/src/common/system.rs @@ -1,6 +1,6 @@ use crate::common::text::strip_trailing_newline; +use std::process::Command; use std::str; -use std::{process::Command, str::FromStr}; pub struct SystemPaths {} diff --git a/src/domain/entity/tag.rs b/src/domain/entity/tag.rs index c770119..ebe41fd 100644 --- a/src/domain/entity/tag.rs +++ b/src/domain/entity/tag.rs @@ -1,9 +1,11 @@ #[derive(Debug)] +#[allow(dead_code)] pub struct Tag { name: String, } impl Tag { + #[allow(dead_code)] pub fn new() -> Self { Tag { name: String::from(""), diff --git a/src/domain/entity/workspace.rs b/src/domain/entity/workspace.rs index 68dd6e7..9ec2a62 100644 --- a/src/domain/entity/workspace.rs +++ b/src/domain/entity/workspace.rs @@ -2,10 +2,7 @@ use crate::domain::value_object::WorkspaceJson; use rusqlite::Row; use urlencoding::decode; -use std::{ - hash::{Hash, Hasher}, - path::Path, -}; +use std::hash::{Hash, Hasher}; /// Workspace Location enumerate #[derive(Debug, Clone, PartialEq, Eq)] @@ -17,12 +14,16 @@ pub enum WorkspaceLocation { /// Implement default associate function for Workspace Location enumerate impl WorkspaceLocation { + /// Default value of WorkspaceLocation + /// #[allow(dead_code)] fn default() -> Self { WorkspaceLocation::NotRecognize } } +/// An explicit conversion from a &str to WorkspaceLocation impl From<&str> for WorkspaceLocation { + /// Generate WorkspaceLocation from &str fn from(path: &str) -> Self { if path.starts_with("file://") { WorkspaceLocation::Local @@ -44,6 +45,7 @@ pub struct Workspace { pub title: String, } +/// Implement Hash for HashSet. Make Workspace a hashable type. impl Hash for Workspace { fn hash(&self, state: &mut H) { self.path.hash(state); @@ -52,6 +54,7 @@ impl Hash for Workspace { /// Implement default associate function for Workspace Location enumerate impl Workspace { + #[allow(dead_code)] pub fn new() -> Self { Workspace { path: String::new(), @@ -63,14 +66,16 @@ impl Workspace { pub fn from_dbrow(row: &Row) -> Self { let raw_path: String = row.get(0).expect("msg"); + let decode_path = decode(raw_path.as_str()).expect("UTF-8").to_string(); Workspace { path: raw_path.clone(), - decode_path: decode(raw_path.as_str()).expect("UTF-8").to_string(), + decode_path: decode_path.clone(), location_type: raw_path.as_str().into(), - title: Path::new(&raw_path) - .file_stem() - .unwrap() - .to_str() + title: decode_path + .clone() + .split("/") + .collect::>() + .last() .unwrap() .to_string(), } @@ -87,16 +92,16 @@ impl From for Workspace { fn from(_wj: WorkspaceJson) -> Self { let decode_folder_path = decode(_wj.folder.as_str()).expect("UTF-8").to_string(); let location = WorkspaceLocation::from(_wj.folder.as_str()); - let workspace_path_obj = Path::new(decode_folder_path.as_str()); Workspace { path: _wj.folder, decode_path: decode_folder_path.clone(), location_type: location, - title: workspace_path_obj - .file_stem() - .unwrap() - .to_str() + title: decode_folder_path + .clone() + .split("/") + .collect::>() + .last() .unwrap() .to_string(), } @@ -108,16 +113,16 @@ impl From<&str> for Workspace { let decode_folder_path = decode(raw_path).expect("UTF-8").to_string(); let location = WorkspaceLocation::from(raw_path); - let workspace_path_obj = Path::new(raw_path); Workspace { path: raw_path.to_string(), - decode_path: decode_folder_path, + decode_path: decode_folder_path.clone(), location_type: location, - title: workspace_path_obj - .file_stem() - .unwrap() - .to_str() + title: decode_folder_path + .clone() + .split("/") + .collect::>() + .last() .unwrap() .to_string(), } diff --git a/src/domain/mod.rs b/src/domain/mod.rs index e1fb4bc..a7ee7d5 100644 --- a/src/domain/mod.rs +++ b/src/domain/mod.rs @@ -1,4 +1,5 @@ pub mod entity; pub mod repository; +pub mod searching; pub mod system; pub mod value_object; diff --git a/src/domain/repository/base.rs b/src/domain/repository/base.rs index 91b9a52..7e1ea08 100644 --- a/src/domain/repository/base.rs +++ b/src/domain/repository/base.rs @@ -1,7 +1,7 @@ pub trait Repository { type EntityType; fn get_item_by_id(&self, id: &str) -> Self::EntityType; - fn get_all_items(&self, id: &str) -> Vec; + fn get_all_items(&self) -> Vec; fn insert_or_create(&self, entity: &Self::EntityType) -> Self::EntityType; fn delete(&self, entity: &Self::EntityType) -> bool; fn delete_entities(&self, entities: &Vec) -> bool; diff --git a/src/domain/searching/mod.rs b/src/domain/searching/mod.rs new file mode 100644 index 0000000..ea86848 --- /dev/null +++ b/src/domain/searching/mod.rs @@ -0,0 +1 @@ +pub mod parse; diff --git a/src/domain/searching/parse.rs b/src/domain/searching/parse.rs new file mode 100644 index 0000000..2e13d03 --- /dev/null +++ b/src/domain/searching/parse.rs @@ -0,0 +1,137 @@ +use regex::Regex; + +#[derive(Debug, PartialEq)] +pub enum SearchingStrategyType { + All, + Tags, + PlainText, + PlainTextMixTags, +} + +impl SearchingStrategyType { + pub fn parse(path_length: usize, tags_count: usize) -> SearchingStrategyType { + match (path_length > 0, tags_count > 0) { + (true, true) => SearchingStrategyType::PlainTextMixTags, + (true, false) => SearchingStrategyType::PlainText, + (false, true) => SearchingStrategyType::Tags, + (false, false) => SearchingStrategyType::All, + } + } +} + +/// Contain and Parse searching strategy information +#[derive(Debug, PartialEq)] +pub struct SearchingStrategy { + path: String, + tags: Vec, + pub searching_type: SearchingStrategyType, +} + +impl SearchingStrategy { + #[allow(dead_code)] + pub fn default() -> SearchingStrategy { + SearchingStrategy { + path: String::new(), + tags: vec![], + searching_type: SearchingStrategyType::All, + } + } + + #[allow(dead_code)] + pub fn new(origin: &str) -> SearchingStrategy { + origin.into() + } +} + +impl From for SearchingStrategy { + fn from(origin: String) -> Self { + origin.as_str().into() + } +} + +impl From<&str> for SearchingStrategy { + fn from(origin: &str) -> Self { + let tag_re = Regex::new(r"#[a-zA-Z0-9]+").unwrap(); + let tags = tag_re + .captures_iter(origin) + .map(|x| x.get(0).unwrap().as_str()) + .map(|x| x.to_string().replace(" ", "").replace("#", "")) + .filter(|x| x.len() > 0) + .collect::>(); + + let filtered_text = tag_re.replace_all(origin, ""); + let tags_count = tags.len(); + + SearchingStrategy { + path: String::from(filtered_text.clone()), + tags: tags, + searching_type: SearchingStrategyType::parse(filtered_text.clone().len(), tags_count), + } + } +} + +impl SearchingStrategy { + // pub fn filter +} + +#[cfg(test)] +mod test_searching_strategy_mod { + + use super::*; + + #[test] + fn test_default_str_into_searching_strategy() { + assert_eq!(SearchingStrategy::default(), "".into()); + } + + #[test] + fn test_spaces_str_into_searching_strategy() { + assert_eq!( + SearchingStrategy { + path: String::from(" "), + tags: vec![], + searching_type: SearchingStrategyType::PlainText + }, + " ".into() + ); + } + #[test] + fn test_empty_tagname_str_into_searching_strategy() { + assert_eq!( + SearchingStrategy { + path: String::from(" #"), + tags: vec![], + searching_type: SearchingStrategyType::PlainText + }, + " #".into() + ); + } + #[test] + fn test_mix_tagname_str_into_searching_strategy() { + assert_eq!( + SearchingStrategy { + path: String::from(" "), + tags: vec![String::from("XD"), String::from("QQ")], + searching_type: SearchingStrategyType::PlainTextMixTags + }, + "#XD #QQ".into() + ); + } + #[test] + fn test_only_tag_str_into_searching_strategy() { + assert_eq!( + SearchingStrategy { + path: String::from(""), + tags: vec![String::from("XD"), String::from("ABC"), String::from("QQ")], + searching_type: SearchingStrategyType::Tags + }, + "#XD#ABC#QQ".into() + ); + } + + /// String into searchingStrategy + #[test] + fn test_string_into_default_searching_strategy() { + assert_eq!(SearchingStrategy::default(), String::from("").into()); + } +} diff --git a/src/domain/system/error.rs b/src/domain/system/error.rs index b2e50d0..f991f74 100644 --- a/src/domain/system/error.rs +++ b/src/domain/system/error.rs @@ -1,6 +1,7 @@ use thiserror::Error; #[derive(Error, Debug)] +#[allow(dead_code)] pub enum ApplicationInitError { #[error("")] CannotCreateDatabaseFolder, diff --git a/src/domain/system/init.rs b/src/domain/system/init.rs index d0be224..745d838 100644 --- a/src/domain/system/init.rs +++ b/src/domain/system/init.rs @@ -5,7 +5,7 @@ use crate::common::system::SystemPaths; pub fn init_application_folders() -> Result<(), ApplicationInitError> { let database_path = SystemPaths::database_folder(); - fs::create_dir_all(database_path); + fs::create_dir_all(database_path).expect("Cannot create application folders."); Ok(()) } diff --git a/src/domain/system/scan.rs b/src/domain/system/scan.rs index 953e2cb..19ce831 100644 --- a/src/domain/system/scan.rs +++ b/src/domain/system/scan.rs @@ -1,15 +1,16 @@ +use crossbeam_channel::bounded; +use glob::glob; use std::io::Error; use std::str::FromStr; +use std::thread; use std::{fs, str}; extern crate glob; + use crate::common::system::SystemPaths; use crate::domain::entity::workspace::Workspace; use crate::domain::value_object::WorkspaceJson; -use glob::glob; -use std::thread; fn scan_vscode_workspacestorage_from_system() -> Result, Error> { - let home = SystemPaths::home_dir(); let tasks = glob(SystemPaths::vscode_workspace_storage_path().as_str()) .expect("Fali to read glob pattern") .into_iter() @@ -28,8 +29,6 @@ fn extract_json_file(path: &str) -> Option { } pub fn scan_workspaces_path() -> Vec { - use crossbeam_channel::{bounded, Sender}; - // Get all vscode workspace json files path let current_workspaces_list: Result, Error> = scan_vscode_workspacestorage_from_system(); diff --git a/src/infrastructure/repository/error.rs b/src/infrastructure/repository/error.rs index 0092d08..c43450f 100644 --- a/src/infrastructure/repository/error.rs +++ b/src/infrastructure/repository/error.rs @@ -1,8 +1,8 @@ use thiserror::Error; #[derive(Error, Debug)] +#[allow(dead_code)] pub enum DatabaseError { - #[error("Can't open database file")] CannotOpenDatabaseFile(#[from] rusqlite::Error), diff --git a/src/infrastructure/repository/macros.rs b/src/infrastructure/repository/macros.rs new file mode 100644 index 0000000..1bb43c1 --- /dev/null +++ b/src/infrastructure/repository/macros.rs @@ -0,0 +1,33 @@ +#[macro_export] +macro_rules! filter_sql { + ($table:expr) => { + format!("select * from {}", $table) + }; + ($table:expr, $($condiction:expr),* ) => { + { + let conds = vec![ + $($condiction.to_string() , )+ + ]; + format!("{}{}", format!("select * from {} where ", $table), conds.join(" & ")) + } + }; +} + +#[cfg(test)] +mod test_marcos { + #[test] + fn select_all_sql_should_be_correct() { + assert_eq!( + filter_sql!("TestTable"), + String::from("select * from TestTable") + ); + } + + #[test] + fn select_with_filter_sql_should_be_correct() { + assert_eq!( + filter_sql!("TestTable", "time=1", "done = 2"), + String::from("select * from TestTable where time=1 & done = 2") + ); + } +} diff --git a/src/infrastructure/repository/mod.rs b/src/infrastructure/repository/mod.rs index a52e7bd..1e89b41 100644 --- a/src/infrastructure/repository/mod.rs +++ b/src/infrastructure/repository/mod.rs @@ -1,4 +1,5 @@ pub mod error; +pub mod macros; pub mod workspace_repository; use self::error::DatabaseError; @@ -34,17 +35,17 @@ CREATE INDEX idx_tags ON tags_workspaces(tags_id); pub fn create_database(path: &str) -> Result { let db_connection: Connection = match Connection::open(path) { Ok(con) => con, - Err(err) => { + Err(_err) => { println!("Open error"); Connection::open(path)? } }; match db_connection.execute(SQL, ()) { - Ok(val) => { + Ok(_val) => { // successfully create table } - Err(e) => { + Err(_e) => { // table already existed } } @@ -55,7 +56,7 @@ pub fn create_database(path: &str) -> Result { pub fn get_db_connection(path: &str) -> Result { let db_connection: Connection = match Connection::open(path) { Ok(con) => con, - Err(err) => Connection::open(path)?, + Err(_err) => Connection::open(path)?, }; Ok(db_connection) diff --git a/src/infrastructure/repository/schema.sql b/src/infrastructure/repository/schema.sql index e26212e..256be3b 100644 --- a/src/infrastructure/repository/schema.sql +++ b/src/infrastructure/repository/schema.sql @@ -1,13 +1,13 @@ CREATE TABLE workspaces ( id INTEGER PRIMARY KEY AUTOINCREMENT, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - path TEXT NOT NULL path TEXT NOT NULL type TEXT NOT NULL + path TEXT NOT NULL ); CREATE TABLE tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - name TEXT NOT NULL, + name TEXT NOT NULL ); CREATE TABLE tags_workspaces ( diff --git a/src/infrastructure/repository/workspace_repository.rs b/src/infrastructure/repository/workspace_repository.rs index 9cbf417..43104dd 100644 --- a/src/infrastructure/repository/workspace_repository.rs +++ b/src/infrastructure/repository/workspace_repository.rs @@ -1,4 +1,5 @@ use rusqlite::params; +use std::collections::HashSet; use crate::application::error::ApplicationError; use crate::common::system::SystemPaths; @@ -7,20 +8,16 @@ use crate::domain::repository::base::Repository; use super::get_db_connection; -use core::time; -use std::collections::HashSet; -use std::thread; - pub struct WorkspaceRepository {} impl Repository for WorkspaceRepository { type EntityType = Workspace; - fn get_item_by_id(&self, id: &str) -> Self::EntityType { + fn get_item_by_id(&self, _id: &str) -> Self::EntityType { todo!() } - fn get_all_items(&self, id: &str) -> Vec { + fn get_all_items(&self) -> Vec { let db_connection = get_db_connection(SystemPaths::database().as_str()) .expect("Cannot get database connection."); @@ -31,7 +28,10 @@ impl Repository for WorkspaceRepository { let workspace_iter = stmt .query_map([], |row| Ok(Workspace::from_dbrow(row))) .expect("Failed to transform database row to entity."); - vec![] + + workspace_iter + .map(|x| x.unwrap()) + .collect::>() } fn insert_or_create(&self, entity: &Self::EntityType) -> Self::EntityType { @@ -48,7 +48,7 @@ impl Repository for WorkspaceRepository { Self::EntityType::from(entity.path.as_str()) } - fn delete(&self, entity: &Self::EntityType) -> bool { + fn delete(&self, _entity: &Self::EntityType) -> bool { let db_connection = get_db_connection(SystemPaths::database().as_str()) .expect("Cannot get database connection."); @@ -56,7 +56,10 @@ impl Repository for WorkspaceRepository { .prepare(r#"DELETE FROM workspaces where"#) .expect("Failed to select all workspaces."); - true + match stmt.execute(()) { + Ok(_) => true, + Err(_) => false, + } } fn delete_entities(&self, entities: &Vec) -> bool { @@ -68,7 +71,7 @@ impl Repository for WorkspaceRepository { .expect("Failed to select all workspaces."); for entity in entities { - stmt.execute([entity.path.clone()]); + stmt.execute([entity.path.clone()]).unwrap(); } true @@ -76,6 +79,25 @@ impl Repository for WorkspaceRepository { } impl WorkspaceRepository { + #[allow(dead_code)] + pub fn filter(sql: String) -> Result, ApplicationError> { + let db_connection = get_db_connection(SystemPaths::database().as_str()) + .expect("Cannot get database connection."); + + println!("{sql}"); + let mut stmt = db_connection + .prepare(&sql) + .expect("Failed to select all workspaces."); + + let workspace_iter = stmt + .query_map([], |row| Ok(Workspace::from_dbrow(row))) + .expect("Failed to transform database row to entity."); + + Ok(workspace_iter + .map(|x| x.unwrap()) + .collect::>()) + } + pub fn sync_to_database( curr_workspaces: &Vec, ) -> Result, ApplicationError> { diff --git a/src/presentation/management.rs b/src/presentation/management.rs index 536b165..0d0d404 100644 --- a/src/presentation/management.rs +++ b/src/presentation/management.rs @@ -25,19 +25,31 @@ use tui::{ use crate::application::app::App; -use super::text::{DETAIL_MODE_HELP_TEXT, SEARCH_MODE_HELP_TEXT}; +use super::{ + style::RuscodeStyle, + text::{DETAIL_MODE_HELP_TEXT, SEARCH_MODE_HELP_TEXT}, +}; /// Display detail information of selected vscode workspace /// -pub fn draw_management_content_info_block(f: &mut Frame, area: Rect) +pub fn draw_management_content_info_block(f: &mut Frame, app: &mut App, area: Rect) where B: Backend, { + // Split area in to chunks let chunks = Layout::default() .constraints([Constraint::Min(30)].as_ref()) .split(area); let p = Paragraph::new("Workspace detail 🔍") + .style(match app.control_mode { + crate::application::app::ApplicationControlMode::SearchMode => { + RuscodeStyle::unfocus_mode() + } + crate::application::app::ApplicationControlMode::DetailMode => { + RuscodeStyle::focus_mode() + } + }) .alignment(Alignment::Center) .block( Block::default() @@ -52,6 +64,7 @@ pub fn draw_management_control_block(f: &mut Frame, app: &mut App, area: R where B: Backend, { + // Split area in to chunks let chunks = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Percentage(30), Constraint::Percentage(70)].as_ref()) @@ -66,18 +79,45 @@ fn draw_management_control_upper_bar(f: &mut Frame, app: &mut App, area: R where B: Backend, { + // Split area in to chunks let chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Percentage(50), Constraint::Percentage(50)].as_ref()) .split(area); - let p = Paragraph::new(app.search_text.clone()) + // Define and render "Search bar" block + let p = Paragraph::new(app.search_text.as_ref()) + .style(match app.control_mode { + crate::application::app::ApplicationControlMode::SearchMode => { + RuscodeStyle::default_focus_mode() + } + crate::application::app::ApplicationControlMode::DetailMode => { + RuscodeStyle::unfocus_mode() + } + }) .block(Block::default().borders(Borders::ALL).title(" Search ")); f.render_widget(p, chunks[0]); - let help_text_paragraph = - Paragraph::new(DETAIL_MODE_HELP_TEXT).block(Block::default().borders(Borders::ALL)); + // Define and render "help text" block + let help_text_paragraph = Paragraph::new(match app.control_mode { + crate::application::app::ApplicationControlMode::SearchMode => SEARCH_MODE_HELP_TEXT, + crate::application::app::ApplicationControlMode::DetailMode => DETAIL_MODE_HELP_TEXT, + }) + .block(Block::default().borders(Borders::ALL)) + .style(RuscodeStyle::success()); f.render_widget(help_text_paragraph, chunks[1]); + + // If the application is currently in Search mode ( which means search text is not empty ) + // then, use UnicodeWidth to control position of cursor + if app.search_text.len() > 0 { + use unicode_width::UnicodeWidthStr; + f.set_cursor( + // Put cursor past the end of the input text + chunks[0].x + app.search_text.width() as u16 + 1, + // Move one line down, from the border to the input line + chunks[0].y + 1, + ) + } } /// Render vscode workspace management tab UI @@ -85,10 +125,13 @@ fn draw_management_control_workspace_list(f: &mut Frame, app: &mut App, ar where B: Backend, { + // Split area in to chunks let chunks = Layout::default() .constraints([Constraint::Min(0)].as_ref()) .split(area); + app.workspaces.change_item_source(app.filtered_workspaces()); + let items = List::new( app.workspaces .items @@ -104,10 +147,23 @@ where Style::default().add_modifier(Modifier::DIM), )), ]; - ListItem::new(lines).style(Style::default().fg(Color::White)) + ListItem::new(lines).style(match app.control_mode { + crate::application::app::ApplicationControlMode::SearchMode => { + RuscodeStyle::default_font() + } + crate::application::app::ApplicationControlMode::DetailMode => { + RuscodeStyle::unfocus_mode() + } + }) }) .collect::>(), ) + .style(match app.control_mode { + crate::application::app::ApplicationControlMode::SearchMode => { + RuscodeStyle::default_focus_mode() + } + crate::application::app::ApplicationControlMode::DetailMode => RuscodeStyle::unfocus_mode(), + }) .block( Block::default() .borders(Borders::ALL) diff --git a/src/presentation/mod.rs b/src/presentation/mod.rs index bf2e634..5f092b5 100644 --- a/src/presentation/mod.rs +++ b/src/presentation/mod.rs @@ -1,3 +1,4 @@ mod management; +pub mod style; mod text; pub mod ui; diff --git a/src/presentation/style.rs b/src/presentation/style.rs new file mode 100644 index 0000000..d595072 --- /dev/null +++ b/src/presentation/style.rs @@ -0,0 +1,25 @@ +use tui::style::{Color, Modifier, Style}; + +pub struct RuscodeStyle {} + +impl RuscodeStyle { + pub fn unfocus_mode() -> Style { + Style::default() + .fg(Color::DarkGray) + .add_modifier(Modifier::DIM) + } + pub fn focus_mode() -> Style { + Style::default().fg(Color::Rgb(122, 171, 212)) + // Style::default().fg(Color::White) + } + pub fn default_focus_mode() -> Style { + Style::default().fg(Color::Rgb(212, 212, 212)) + } + pub fn success() -> Style { + Style::default().fg(Color::Rgb(169, 211, 171)) + } + + pub fn default_font() -> Style { + Style::default().fg(Color::Rgb(212, 212, 212)) + } +} diff --git a/src/presentation/text.rs b/src/presentation/text.rs index 5f1cb6e..3844610 100644 --- a/src/presentation/text.rs +++ b/src/presentation/text.rs @@ -1,11 +1,11 @@ pub const SEARCH_MODE_HELP_TEXT: &str = r#"Arrow key ▲▼: Move arrow -Arrow key ◄►: Enter or leave workspace detail +Arrow key ►: Enter workspace detail Enter Key ↩ : open workspace -Type Anything to search!! +Type anything to search!! "#; pub const DETAIL_MODE_HELP_TEXT: &str = r#"Arrow key ▲▼: Move arrow -Arrow key ◄►: Enter or leave workspace detail -Enter Key ↩ : open workspace -Type Anything to search!! +Arrow key ◄: Leave workspace detail +Enter Key ↩ : Add new tag to workspace +Type anything for tag name!! "#; diff --git a/src/presentation/ui.rs b/src/presentation/ui.rs index c634e85..297f2aa 100644 --- a/src/presentation/ui.rs +++ b/src/presentation/ui.rs @@ -1,8 +1,3 @@ -use std::{ - cell::RefCell, - sync::{Arc, Mutex}, -}; - use crate::application::app::App; use tui::{ backend::Backend, @@ -85,7 +80,7 @@ where /// # Arguments /// * `f` - Franme /// * `app` - App struct -fn draw_splash_screen(f: &mut Frame, app: &mut App) +fn draw_splash_screen(f: &mut Frame, _app: &mut App) where B: Backend, { @@ -116,7 +111,7 @@ where .split(area); draw_management_control_block(f, app, chunks[0]); - draw_management_content_info_block(f, chunks[1]); + draw_management_content_info_block(f, app, chunks[1]); } /// Render ruscode setting tab UI @@ -125,11 +120,11 @@ where /// * `f` - Franme /// * `app` - App struct /// * `area` - area of frame -fn draw_settings_tab(f: &mut Frame, _app: &mut App, area: Rect) +fn draw_settings_tab(_f: &mut Frame, _app: &mut App, area: Rect) where B: Backend, { - let chunks = Layout::default() + let _chunks = Layout::default() .direction(Direction::Horizontal) .constraints([Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)]) .split(area);