added function to wrap debug messages in an echo command
implemented formatted printing of stack and bookmarks
This commit is contained in:
@@ -5,5 +5,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.0", features = ["derive"] }
|
||||
dirs = "5.0.1"
|
||||
serde = { version = "1.0.216", features = [ "std", "derive" ] }
|
||||
sysinfo = "0.32.0"
|
||||
toml = "0.8.19"
|
||||
|
||||
+3
-4
@@ -1,3 +1,6 @@
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_camel_case_types)]
|
||||
|
||||
use clap::{Args, Parser, Subcommand};
|
||||
use std::path::PathBuf;
|
||||
|
||||
@@ -15,7 +18,6 @@ pub struct Arguments {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum Action {
|
||||
/// navigate to path and add current path to the stack
|
||||
push(PushArgs),
|
||||
@@ -55,7 +57,6 @@ pub struct PopArgs {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum PopAction {
|
||||
/// pop all entries and move to first entry in stack
|
||||
all,
|
||||
@@ -77,7 +78,6 @@ pub struct StackArgs {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum StackAction {
|
||||
/// clear stack
|
||||
clear(EmptyArgs),
|
||||
@@ -94,7 +94,6 @@ pub struct BookmarkArgs {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Subcommand)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum BookmarkAction {
|
||||
/// list all bookmarks
|
||||
list(EmptyArgs),
|
||||
|
||||
+54
-17
@@ -9,7 +9,9 @@ use std::io::{Error, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
|
||||
use crate::{RESET_SEQ, STYLES};
|
||||
use crate::make_padding_string;
|
||||
|
||||
use super::{apply_format, config::*};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Bookmarks {
|
||||
@@ -80,23 +82,15 @@ impl Bookmarks {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// writes the bookmarks file
|
||||
fn write_bookmark_file(&self) -> Result<()> {
|
||||
let mut file_content = String::new();
|
||||
for (mark, path) in self.bookmarks.iter() {
|
||||
file_content.push_str(&format!("{}={}\n", mark, path.to_str().unwrap()));
|
||||
/// returns path of bookmark if it exists
|
||||
pub fn get_path_by_name(&mut self, name: &str) -> Result<PathBuf> {
|
||||
match self.bookmarks.get(name) {
|
||||
Some(value) => Ok(value.to_owned()),
|
||||
None => Err(Error::other(format!(
|
||||
"-- bookmark with name `{}` does not exist",
|
||||
name
|
||||
))),
|
||||
}
|
||||
|
||||
let mut path = self.conf_dir.clone();
|
||||
path.push(Self::BOOKMARK_FILE_NAME);
|
||||
|
||||
fs::write(path, file_content)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// returns a mutable reference to self.bookmarks
|
||||
pub fn get_bookmarks(&mut self) -> &mut HashMap<String, PathBuf> {
|
||||
&mut self.bookmarks
|
||||
}
|
||||
|
||||
/// adds a key/value pair to bookmarks and writes the bookmarks file
|
||||
@@ -124,4 +118,47 @@ impl Bookmarks {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// formats and prints bookmarks to string
|
||||
pub fn to_formatted_string(&self, config: &Settings) -> Result<String> {
|
||||
let mut buffer = String::new();
|
||||
|
||||
if self.bookmarks.is_empty() {
|
||||
buffer.push_str("-- there are no bookmarks defined");
|
||||
} else {
|
||||
let max_name_len = match self.bookmarks.keys().map(String::len).max() {
|
||||
Some(value) => value,
|
||||
None => return Err(Error::other("-- failed to determine maximum bookmark name length")),
|
||||
};
|
||||
for (mark, path) in &self.bookmarks {
|
||||
let padding = make_padding_string(max_name_len - mark.len());
|
||||
let name = apply_format(mark, &config.styles.bookmarks_name);
|
||||
let separator = apply_format(
|
||||
&config.format.bookmarks_separator,
|
||||
&config.styles.bookmarks_seperator,
|
||||
);
|
||||
let path = apply_format(path.to_str().unwrap(), &config.styles.bookmarks_path);
|
||||
if config.format.align_separators {
|
||||
buffer.push_str(&format!("{}{}{}{}\n", name, padding, separator, path));
|
||||
} else {
|
||||
buffer.push_str(&format!("{}{}{}{}\n", name, separator, padding, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// writes the bookmarks file
|
||||
fn write_bookmark_file(&self) -> Result<()> {
|
||||
let mut file_content = String::new();
|
||||
for (mark, path) in self.bookmarks.iter() {
|
||||
file_content.push_str(&format!("{}={}\n", mark, path.to_str().unwrap()));
|
||||
}
|
||||
|
||||
let mut path = self.conf_dir.clone();
|
||||
path.push(Self::BOOKMARK_FILE_NAME);
|
||||
|
||||
fs::write(path, file_content)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
+105
-41
@@ -1,54 +1,54 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
//! handle the config file and bookmarks stored
|
||||
//! in said config file
|
||||
|
||||
use crate::format::*;
|
||||
use dirs::config_dir;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fs;
|
||||
use std::io::{Error, Result};
|
||||
use std::path::PathBuf;
|
||||
use std::env::var;
|
||||
use std::fs;
|
||||
use std::str::FromStr;
|
||||
use toml::{from_str, Table};
|
||||
|
||||
use crate::{RESET_SEQ, STYLES};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Config {
|
||||
conf_dir: PathBuf,
|
||||
conf_file: PathBuf,
|
||||
pub settings: Settings,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct Settings {
|
||||
pub general: GeneralSettings,
|
||||
pub format: FormatSettings,
|
||||
pub styles: StyleSettings,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct GeneralSettings {
|
||||
pub show_stack_on_push: bool,
|
||||
pub show_stack_on_pop: bool,
|
||||
pub show_stack_on_bookmark: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct FormatSettings {
|
||||
pub stack_separator: String,
|
||||
pub bookmarks_separator: String,
|
||||
pub align_separators: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct StyleSettings {
|
||||
pub stack_number: String,
|
||||
pub stack_separator: String,
|
||||
pub stack_path: String,
|
||||
pub bookmarks_key: String,
|
||||
pub bookmarks_name: String,
|
||||
pub bookmarks_seperator: String,
|
||||
pub bookmarks_path: String,
|
||||
pub reset: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
@@ -57,7 +57,7 @@ impl Config {
|
||||
/// generates and populates a new instance of Config
|
||||
pub fn new() -> Result<Self> {
|
||||
let mut config = Config {
|
||||
conf_dir: PathBuf::new(),
|
||||
conf_file: PathBuf::new(),
|
||||
settings: Settings {
|
||||
general: GeneralSettings {
|
||||
show_stack_on_push: false,
|
||||
@@ -65,47 +65,111 @@ impl Config {
|
||||
show_stack_on_bookmark: false,
|
||||
},
|
||||
format: FormatSettings {
|
||||
bookmarks_separator: " - ".to_owned(),
|
||||
stack_separator: " - ".to_owned(),
|
||||
bookmarks_separator: String::new(),
|
||||
stack_separator: String::new(),
|
||||
align_separators: false,
|
||||
},
|
||||
styles: StyleSettings {
|
||||
stack_number: "".to_owned(),
|
||||
stack_separator: "".to_owned(),
|
||||
stack_path: "".to_owned(),
|
||||
bookmarks_key: "".to_owned(),
|
||||
bookmarks_seperator: "".to_owned(),
|
||||
bookmarks_path: "".to_owned(),
|
||||
reset: RESET_SEQ.to_owned(),
|
||||
stack_number: String::new(),
|
||||
stack_separator: String::new(),
|
||||
stack_path: String::new(),
|
||||
bookmarks_name: String::new(),
|
||||
bookmarks_seperator: String::new(),
|
||||
bookmarks_path: String::new(),
|
||||
},
|
||||
},
|
||||
};
|
||||
// get home directory path
|
||||
let home_dir = match var("HOME") {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(Error::other(error.to_string())),
|
||||
// get configuration directory
|
||||
config.conf_file = match config_dir() {
|
||||
Some(value) => value,
|
||||
None => {
|
||||
return Err(Error::other(
|
||||
"-- failed to retrieve configuration directory",
|
||||
))
|
||||
}
|
||||
};
|
||||
// create PathBuf object from home dir path
|
||||
config.conf_dir = match PathBuf::from_str(&home_dir) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(Error::other(error.to_string())),
|
||||
};
|
||||
// expand home directory path to get configuration directory path
|
||||
config.build_config()?;
|
||||
// expand path to configuration file
|
||||
config
|
||||
.conf_file
|
||||
.push(format!("navigate/{}", Self::CONFIG_FILE_NAME));
|
||||
|
||||
// parse configuration file and populate config struct
|
||||
config.build_settings()?;
|
||||
config.set_default_settings()?;
|
||||
config.write_config_file()?;
|
||||
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// formats and prints config to string
|
||||
pub fn to_formatted_string(&self) -> Result<String> {
|
||||
let mut buffer = String::new();
|
||||
buffer = format!("{:#?}", self.settings);
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// write configuration file to save changed settings
|
||||
pub fn write_config_file(&self) -> Result<()> {
|
||||
let conf_str = match toml::to_string(&self.settings) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(Error::other(error.to_string())),
|
||||
};
|
||||
fs::write(self.conf_file.clone(), conf_str)
|
||||
}
|
||||
|
||||
/// reads and parses the configuration file
|
||||
fn build_config(&mut self) -> Result<()> {
|
||||
let config_file = match fs::read_to_string(&self.conf_dir) {
|
||||
fn build_settings(&mut self) -> Result<()> {
|
||||
if !self.conf_file.is_file() {
|
||||
return Ok(());
|
||||
}
|
||||
let config_str = match fs::read_to_string(&self.conf_file) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let conf_table = match config_file.parse::<Table>() {
|
||||
self.settings = match toml::from_str(&config_str) {
|
||||
Ok(value) => value,
|
||||
Err(error) => return Err(Error::other(error.to_string())),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// sets defaults for settings not found in the configuration file
|
||||
fn set_default_settings(&mut self) -> Result<()> {
|
||||
let default_separator = " - ".to_owned();
|
||||
let default_number_color =
|
||||
generate_style_sequence(None, Some(COLORS.fg.default), Some(COLORS.bg.default));
|
||||
let default_separator_color =
|
||||
generate_style_sequence(None, Some(COLORS.fg.cyan), Some(COLORS.bg.default));
|
||||
let default_path_color =
|
||||
generate_style_sequence(None, Some(COLORS.fg.default), Some(COLORS.bg.default));
|
||||
|
||||
if self.settings.format.stack_separator.is_empty() {
|
||||
self.settings.format.stack_separator = default_separator.clone();
|
||||
}
|
||||
if self.settings.format.bookmarks_separator.is_empty() {
|
||||
self.settings.format.bookmarks_separator = default_separator.clone();
|
||||
}
|
||||
|
||||
if self.settings.styles.stack_number.is_empty() {
|
||||
self.settings.styles.stack_number = default_number_color.clone();
|
||||
}
|
||||
if self.settings.styles.stack_separator.is_empty() {
|
||||
self.settings.styles.stack_separator = default_separator_color.clone();
|
||||
}
|
||||
if self.settings.styles.stack_path.is_empty() {
|
||||
self.settings.styles.stack_path = default_path_color.clone();
|
||||
}
|
||||
if self.settings.styles.bookmarks_name.is_empty() {
|
||||
self.settings.styles.bookmarks_name = default_number_color.clone();
|
||||
}
|
||||
if self.settings.styles.bookmarks_seperator.is_empty() {
|
||||
self.settings.styles.bookmarks_seperator = default_separator_color.clone();
|
||||
}
|
||||
if self.settings.styles.bookmarks_path.is_empty() {
|
||||
self.settings.styles.bookmarks_path = default_path_color.clone();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
pub fn debug_print(string: &str) {
|
||||
println!("echo '{}' && ", string);
|
||||
}
|
||||
+15
-2
@@ -1,3 +1,5 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
pub const ESC: &str = "\x1B";
|
||||
pub const PREFIX: &str = "\x1B[";
|
||||
pub const RESET_ARG: &str = "0";
|
||||
@@ -105,6 +107,11 @@ pub enum ColorContext {
|
||||
Background,
|
||||
}
|
||||
|
||||
/// prepends input with style string and appends the reset sequence at the end
|
||||
pub fn apply_format(input: &str, style: &str) -> String {
|
||||
format!("{}{}{}", style, input, RESET_SEQ)
|
||||
}
|
||||
|
||||
/// generates a common style sequence of format
|
||||
/// `\x1B[<styles>;<foreground-color>;<background-color>m`
|
||||
/// all elements are optional, if none is supplied the function returns an error
|
||||
@@ -131,7 +138,7 @@ pub fn generate_style_sequence(
|
||||
}
|
||||
|
||||
// panic if no arguments provided since this is a programming mistake
|
||||
// which should not
|
||||
// which should not
|
||||
if arguments.is_empty() {
|
||||
panic!("no arguments provided to 'generate_style_sequence()'");
|
||||
}
|
||||
@@ -157,7 +164,7 @@ pub fn generate_256color_sequence(context: ColorContext, color: u8) -> String {
|
||||
|
||||
/// generates a rgb color sequence
|
||||
/// **note**: not all terminal emulators support rgb colors
|
||||
///
|
||||
///
|
||||
/// rgb sequences are built the same as 256 color sequences:
|
||||
/// `\x1B[<context>;2;<r>;<g>;<b>m`
|
||||
/// where *context* is either '38' or '48' for foreground and background respectively
|
||||
@@ -174,3 +181,9 @@ pub fn generate_rgb_sequence(context: ColorContext, red: u8, green: u8, blue: u8
|
||||
sequence.push_str(&format!("{red};{green};{blue}m"));
|
||||
sequence
|
||||
}
|
||||
|
||||
/// generates a padding string for numbers in a list
|
||||
pub fn make_padding_string(len: usize) -> String {
|
||||
// determine padding needed to align the paths
|
||||
String::from_utf8(vec![b' '; len]).unwrap()
|
||||
}
|
||||
|
||||
+16
-13
@@ -3,6 +3,7 @@ mod format;
|
||||
mod config;
|
||||
mod bookmarks;
|
||||
mod stack;
|
||||
mod debug;
|
||||
|
||||
use arguments::*;
|
||||
use clap::Parser;
|
||||
@@ -54,6 +55,7 @@ fn main() -> Result<()> {
|
||||
Action::pop(pop_args) => handle_pop(&pop_args, &config, &mut stack),
|
||||
Action::stack(stack_args) => handle_stack(&stack_args, &config, &mut stack),
|
||||
Action::bookmark(bookmark_args) => handle_bookmark(&bookmark_args, &config, &mut bookmarks, &mut stack),
|
||||
// Action::config(config_args) => handle_config(&config_args, &config),
|
||||
};
|
||||
|
||||
if res.is_err() {
|
||||
@@ -108,11 +110,11 @@ fn handle_pop(args: &PopArgs, _config: &Config, stack: &mut Stack) -> Result<()>
|
||||
fn handle_stack(args: &StackArgs, config: &Config, stack: &mut Stack) -> Result<()> {
|
||||
if args.stack_action.is_some() {
|
||||
match args.stack_action.clone().unwrap() {
|
||||
StackAction::clear(_) => return stack.clear_stack(config),
|
||||
StackAction::clear(_) => return stack.clear_stack(&config.settings),
|
||||
}
|
||||
}
|
||||
// retrieve stack
|
||||
let output: String = stack.to_string(None)?;
|
||||
let output: String = stack.to_formatted_string(&config.settings)?;
|
||||
print!("echo '{}'", output);
|
||||
Ok(())
|
||||
}
|
||||
@@ -125,12 +127,9 @@ fn handle_bookmark(args: &BookmarkArgs, config: &Config, bookmarks: &mut Bookmar
|
||||
BookmarkAction::add(args) => add_bookmarks(args, config, bookmarks)?,
|
||||
BookmarkAction::remove(args) => remove_bookmarks(args, config, bookmarks)?,
|
||||
};
|
||||
} else if args.name.is_some() {
|
||||
let path = match bookmarks.get_bookmarks().get(args.name.as_ref().unwrap()) {
|
||||
Some(value) => value,
|
||||
None => return Err(Error::other("-- requested bookmark does not exist")),
|
||||
};
|
||||
push_path(path, stack)?;
|
||||
} else if args.name.is_some() { // handle `change to bookmark`
|
||||
let path = bookmarks.get_path_by_name(args.name.as_ref().unwrap())?;
|
||||
push_path(&path, stack)?;
|
||||
} else {
|
||||
return Err(Error::other(
|
||||
"-- provide either a `subcommand` or a `bookmark name`",
|
||||
@@ -139,12 +138,16 @@ fn handle_bookmark(args: &BookmarkArgs, config: &Config, bookmarks: &mut Bookmar
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// fn handle_config(args: &ConfigArgs, config: &Config) -> Result<()> {
|
||||
// match args {
|
||||
// ConfigAction::show => println!("echo '{}'", config.to_formatted_string()),
|
||||
// }
|
||||
// Ok(())
|
||||
// }
|
||||
|
||||
fn list_bookmarks(config: &Config, bookmarks: &mut Bookmarks) -> Result<()> {
|
||||
let mut buffer = String::new();
|
||||
for (mark, path) in bookmarks.get_bookmarks() {
|
||||
buffer.push_str(&format!("{} : {}\n", mark, path.to_str().unwrap()));
|
||||
}
|
||||
println!("echo '{}'", buffer);
|
||||
let output = bookmarks.to_formatted_string(&config.settings)?;
|
||||
println!("echo '{}'", output);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+32
-15
@@ -1,4 +1,5 @@
|
||||
use super::config::*;
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::fs;
|
||||
use std::fs::File;
|
||||
use std::io::{Error, Result};
|
||||
@@ -6,6 +7,10 @@ use std::path::{Path, PathBuf};
|
||||
use std::str::FromStr;
|
||||
use sysinfo::{Pid, System};
|
||||
|
||||
use crate::make_padding_string;
|
||||
|
||||
use super::{apply_format, config::*};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Stack {
|
||||
pid: u32,
|
||||
@@ -25,25 +30,37 @@ impl Stack {
|
||||
Ok(stack)
|
||||
}
|
||||
|
||||
// return stack
|
||||
pub fn to_string(&self, _settings: Option<Settings>) -> Result<String> {
|
||||
// formats and prints stack to string
|
||||
pub fn to_formatted_string(&self, config: &Settings) -> Result<String> {
|
||||
let mut buffer: String = "".to_string();
|
||||
|
||||
if self.stack.is_empty() {
|
||||
return Err(Error::other("-- the stack is empty"));
|
||||
buffer.push_str("-- the stack is empty");
|
||||
} else {
|
||||
// print stack to string
|
||||
let max_num_len = self.stack.len().to_string().len();
|
||||
for (n, item) in self.stack.iter().rev().enumerate() {
|
||||
let padding = make_padding_string(max_num_len - n.to_string().len());
|
||||
let number = apply_format(&n.to_string(), &config.styles.stack_number);
|
||||
let separator = apply_format(
|
||||
&config.format.stack_separator,
|
||||
&config.styles.stack_separator,
|
||||
);
|
||||
let path = apply_format(item.to_str().unwrap(), &config.styles.stack_path);
|
||||
if config.format.align_separators {
|
||||
buffer.push_str(&format!("{}{}{}{}\n", number, padding, separator, path));
|
||||
} else {
|
||||
buffer.push_str(&format!("{}{}{}{}\n", number, separator, padding, path));
|
||||
}
|
||||
}
|
||||
}
|
||||
// print stack to string
|
||||
let mut output: String = "".to_string();
|
||||
for (n, item) in self.stack.iter().rev().enumerate() {
|
||||
output.push_str(&format!("'{} - {}'\n", n, item.to_str().unwrap()));
|
||||
}
|
||||
Ok(output)
|
||||
Ok(buffer)
|
||||
}
|
||||
|
||||
/// clear stack by deleting the associated stack file
|
||||
pub fn clear_stack(&mut self, _config: &Config) -> Result<()> {
|
||||
pub fn clear_stack(&mut self, config: &Settings) -> Result<()> {
|
||||
fs::remove_file(self.path.clone())?;
|
||||
print!(
|
||||
"echo stack cleared successfully.'"
|
||||
);
|
||||
print!("echo 'stack cleared successfully.'");
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -172,4 +189,4 @@ impl Stack {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
} // end `impl database`
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
- [x] drop stack
|
||||
- [ ] config file
|
||||
- [ ] dedup stack option
|
||||
- [ ] parse config file
|
||||
- [ ] apply config
|
||||
- [ ] parse config file -- partially done, handling of colors to be fixed
|
||||
- [ ] apply config -- partially done
|
||||
- [ ] colored output > make it configurable through config file
|
||||
- [ ] setting for separator string when displaying stack/bookmarks
|
||||
- [x] setting for separator string when displaying stack/bookmarks
|
||||
- [x] bookmarks
|
||||
|
||||
Reference in New Issue
Block a user