Skip to content

Commit 24fc48f

Browse files
authored
Add interactive commands and config setup (#8)
1 parent 0eb38b2 commit 24fc48f

15 files changed

+1240
-471
lines changed

Cargo.lock

+582-267
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+7-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[package]
22
name = "eso-addons"
3-
version = "0.2.0"
4-
authors = ["Damian Czaja <trojan295@gmail.com>"]
3+
version = "0.3.0"
4+
authors = ["Damian Czaja <trojan295@protonmail.com>"]
55
edition = "2018"
66

77
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -16,7 +16,7 @@ path = "src/main.rs"
1616
toml = "0.4"
1717
serde = "1.0"
1818
serde_derive = "1.0"
19-
clap = "3.0.0-beta.2"
19+
clap = "3.0.0-beta.4"
2020
dirs = "3.0"
2121
simple-error = "0.2"
2222
regex = "1"
@@ -25,4 +25,7 @@ openssl = { version = "0.10", features = ["vendored"] }
2525
html5ever = "0.25"
2626
markup5ever_rcdom = "0.1"
2727
zip = "0.5"
28-
tempfile = "3.1"
28+
tempfile = "3.1"
29+
requestty = "0.1"
30+
colored = "2"
31+
prettytable-rs = "^0.8"

README.md

+54-23
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
This repository holds a simple command line ESO Addon Manager written in Rust. With it you can manage addons from [esoui.com](https://www.esoui.com/).
66

7-
The list of addons you want to install, is put in a single configuration file. This means you can save and share your addon configuration with a single file!
7+
The list of addons you want to install is put in a single configuration file. This means you can save and share your addon configuration with a single file!
88

99
- [ESO Addon Manager](#eso-addon-manager)
1010
- [Usage](#usage)
@@ -16,48 +16,79 @@ The list of addons you want to install, is put in a single configuration file. T
1616

1717
## Usage
1818

19-
<a href="https://asciinema.org/a/381564" target="_blank"><img height="500px" src="https://asciinema.org/a/381564.svg" /></a>
20-
19+
<a href="https://asciinema.org/a/431685" target="_blank"><img src="https://asciinema.org/a/431685.svg" /></a>
2120

2221
### Configuration
2322

24-
Create a config file in your user directory:
25-
- Linux - `$HOME/.eso-addons.toml`
23+
Run:
2624

27-
For an example config see [eso-addons.toml](./eso-addons.toml).
25+
```bash
26+
eso-addons list
27+
```
2828

29-
### Install and update addon
29+
to generate the config file. The config file is in your user directory:
30+
- Linux - `/home/<username>/.eso-addons.toml`
31+
- Windows - `C:/Users/<username>/.eso-addons.toml`
3032

31-
To install or update addons, execute:
32-
```
33-
eso-addons update
33+
If necessary, edit the `addonDir` parameter in the config file to the directory, where your ESO addons should be placed:
34+
```toml
35+
addonDir = "/home/damian/drive_c/users/user/My Documents/Elder Scrolls Online/live/AddOns" # edit this, if needed
3436
```
3537

36-
### List addons, show missing or unused dependencies
38+
### Install new addon
3739

38-
To list the installation status of addons and show missing or unused dependencies, execute:
39-
```
40-
eso-addons list
40+
To install a new addon use the `eso-addons add` command:
41+
```bash
42+
❯ eso-addons add
43+
✔ URL of the addon on esoui.com · https://www.esoui.com/downloads/info1536-ActionDurationReminder.html
44+
✔ Is addon only a dependency? · No
45+
🎊 Installed ActionDurationReminder!
4146
```
4247

43-
### Remove addons
48+
### Update installed addons
4449

45-
The Addon Manager can remove addons, which are present in the ESO addon dir, but not listed in the configuration file.
50+
In case you want to update the addons to the newest version execute `eso-addons update`:
51+
```bash
52+
❯ eso-addons update
53+
✔ Updated ActionDurationReminder!
54+
✔ Updated LibAddonMenu-2.0!
55+
```
4656

47-
It can also detect addons, which are an unused dependency, i.e. you installed addon A, because it was a dependency of B. When you remove B, then `eso-addons` will detect that A is not required anymore and can be removed.
57+
### List addons, show missing or unused addon dependencies
4858

49-
To get the list of addons to be removed, execute:
59+
To list the status of all installed addons, show missing or unused dependencies use `eso-addons list`
5060
```
51-
eso-addons clean
61+
❯ eso-addons list
62+
+------------------------+-----------+
63+
| Name | Status |
64+
+------------------------+-----------+
65+
| ActionDurationReminder | INSTALLED |
66+
| LibAddonMenu-2.0 | MISSING |
67+
+------------------------+-----------+
5268
```
5369

54-
To remove the addons, execute:
70+
### Remove addons
71+
72+
To remove an addon use `eso-addons remove`:
73+
```bash
74+
❯ eso-addons remove
75+
✔ Select addon to remove · ActionDurationReminder
76+
✔ Uninstalled ActionDurationReminder!
5577
```
56-
eso-addons clean --remove
78+
79+
There is also the `eso-addons clean` command, which can be used to remove addons, which are not managed by `eso-addons` (i.e. you installed them manually):
80+
```bash
81+
❯ eso-addons clean
82+
🗑 Addons to remove:
83+
- LibAddonMenu-2.0
84+
85+
✔ Do you want to remove these addons? · Yes
86+
87+
✓ LibAddonMenu-2.0 removed!
5788
```
5889

5990
### Backup and share your addon configuration
6091

61-
Just backup the `eso-addons.toml` file and that's it! In case you have to restore the addons (e.g. after a OS reinstall), just use the backup `eso-addons.toml` and install the addons again.
92+
Just backup the `eso-addons.toml` file and that's it! In case you have to restore the addons (e.g. after an OS reinstall), just put the backuped `eso-addons.toml` in [user directory](#configuration) and run `eso-addons update` to install all addons.
6293

63-
You can also share your addon configuration with friends using the same way, by sending them your `eso-addons.toml` file.
94+
You can also share your addon configuration with other people by sending them your `eso-addons.toml` file.

eso-addons.toml

+8-6
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33
# "C:\Users\[account]\Documents\Elder Scrolls Online\live\AddOns" becomes "/Users/[account]/Documents/Elder Scrolls Online/live/AddOns"
44
#
55
addonDir = "/home/damian/Games/the-elder-scrolls-online-tamriel-unlimited/drive_c/users/damian/My Documents/Elder Scrolls Online/live/AddOns"
6-
7-
# addons - list of addons to be installed
8-
# name - name of the addon
9-
# url - download URL of the addon, it is the link under the Download button on ESOUI
10-
# dependency - (default: false) determines, if the addon is a dependency.
11-
# Set this to true, if you don't use the addon standalone, but only as a dependency for another addon
6+
# example for Windows:
7+
#addonDir = "C:/Users/Administrator/My Documents/Elder Scrolls Online/live/AddOns"
8+
9+
# addons - List of addons to be installed
10+
# name - Name of the addon
11+
# url - Download URL of the addon, it is the link under the Download button on ESOUI.
12+
# dependency - (default: false) Determines, if the addon is a dependency.
13+
# Set this to true, if you don't use the addon standalone, but only as a dependency for another addon.
1214
[[addons]]
1315
name = "SkyShards"
1416
url = "https://www.esoui.com/downloads/download128-SkyShards.html"

src/addons.rs

+5-19
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
use crate::htmlparser;
2+
13
use super::errors::ErrorChain;
24
use html5ever::tendril::TendrilSink;
35
use html5ever::{self, tree_builder::TreeBuilderOpts, ParseOpts};
@@ -188,7 +190,7 @@ impl Manager {
188190
}
189191

190192
fn get_cdn_download_link(dom: &RcDom) -> Option<String> {
191-
let node = find_first_in_node(&dom.document, &|node: &Rc<Node>| match &node.data {
193+
let node = htmlparser::find_first_in_node(&dom.document, &|node: &Rc<Node>| match &node.data {
192194
NodeData::Element {
193195
name,
194196
attrs,
@@ -215,28 +217,12 @@ fn get_cdn_download_link(dom: &RcDom) -> Option<String> {
215217
}
216218
}
217219

218-
fn find_first_in_node<T>(node: &Rc<Node>, f: &dyn Fn(&Rc<Node>) -> Option<T>) -> Option<T> {
219-
match f(node) {
220-
Some(x) => Some(x),
221-
None => {
222-
for child in node.children.borrow().iter() {
223-
match find_first_in_node(child, f) {
224-
Some(x) => return Some(x),
225-
None => {}
226-
}
227-
}
228-
229-
None
230-
}
231-
}
232-
}
233-
234220
fn get_root_dir(path: &Path) -> PathBuf {
235221
match path.parent() {
236222
None => path.to_owned(),
237223
Some(parent) => match parent.to_str().unwrap() {
238224
"" => path.to_owned(),
239-
&_ => get_root_dir(parent)
240-
}
225+
&_ => get_root_dir(parent),
226+
},
241227
}
242228
}

src/cli/add.rs

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
use eso_addons::{
2+
addons::Manager,
3+
config::{self, AddonEntry, Config},
4+
errors::ErrorChain,
5+
htmlparser,
6+
};
7+
use html5ever::{tendril::TendrilSink, tree_builder::TreeBuilderOpts, ParseOpts};
8+
use markup5ever_rcdom::{Node, NodeData, RcDom};
9+
use regex::Regex;
10+
use std::{error::Error, path::Path, rc::Rc};
11+
12+
#[derive(Clap)]
13+
pub struct AddCommand {
14+
addon_url: Option<String>,
15+
#[clap(
16+
short,
17+
long,
18+
about = "Indicate, if the addon is only a dependency for another addon"
19+
)]
20+
dependency: Option<bool>,
21+
}
22+
23+
impl AddCommand {
24+
pub fn run(
25+
&mut self,
26+
cfg: &mut Config,
27+
config_filepath: &Path,
28+
addon_manager: &Manager,
29+
) -> Result<(), Box<dyn std::error::Error>> {
30+
let mut entry = self.get_entry()?;
31+
32+
if cfg.addons.iter().find(|el| el.url == entry.url).is_some() {
33+
println!("Addon {} is already installed", &entry.name);
34+
return Ok(());
35+
}
36+
37+
let installed = addon_manager
38+
.download_addon(&entry.url.clone().unwrap())
39+
.chain_err(&format!("while downloading {}", &entry.name))?;
40+
41+
if entry.name != installed.name {
42+
entry.name = installed.name;
43+
}
44+
45+
cfg.addons.push(entry.clone());
46+
47+
config::save_config(config_filepath, &cfg)?;
48+
49+
println!("🎊 Installed {}!", &entry.name);
50+
51+
Ok(())
52+
}
53+
54+
pub fn get_entry(&mut self) -> Result<AddonEntry, Box<dyn Error>> {
55+
if self.addon_url.is_none() {
56+
self.ask_for_fields()
57+
.chain_err(&format!("failed to get parameters"))?;
58+
}
59+
60+
let addon_url = self.addon_url.clone().ok_or("missing addon URL")?;
61+
let dependency = self.dependency.unwrap_or(false);
62+
63+
let mut response = reqwest::blocking::get(&addon_url)?;
64+
65+
let opts = ParseOpts {
66+
tree_builder: TreeBuilderOpts {
67+
drop_doctype: true,
68+
..Default::default()
69+
},
70+
..Default::default()
71+
};
72+
73+
let dom = html5ever::parse_document(RcDom::default(), opts)
74+
.from_utf8()
75+
.read_from(&mut response)?;
76+
77+
let addon_name = get_addon_name(&dom).ok_or(simple_error!("failed to get addon name"))?;
78+
let download_url = get_download_url(&addon_url);
79+
80+
Ok(AddonEntry {
81+
name: addon_name,
82+
url: download_url,
83+
dependency: dependency,
84+
})
85+
}
86+
87+
fn ask_for_fields(&mut self) -> Result<(), Box<dyn std::error::Error>> {
88+
let mut questions = vec![requestty::Question::input("addon_url")
89+
.message("URL of the addon on esoui.com")
90+
.build()];
91+
92+
if self.dependency.is_none() {
93+
questions.push(
94+
requestty::Question::confirm("dependency")
95+
.message("Is addon only a dependency?")
96+
.default(false)
97+
.build(),
98+
);
99+
}
100+
101+
let answers = requestty::prompt(questions)?;
102+
103+
if let Some(addon_url) = answers.get("addon_url") {
104+
self.addon_url = addon_url.as_string().map(|x| x.to_owned());
105+
};
106+
107+
if let Some(dependency) = answers.get("dependency") {
108+
self.dependency = dependency.as_bool();
109+
};
110+
111+
Ok(())
112+
}
113+
}
114+
115+
fn get_addon_name(dom: &RcDom) -> Option<String> {
116+
htmlparser::find_first_in_node(&dom.document, &|node: &Rc<Node>| match &node.data {
117+
NodeData::Element {
118+
name,
119+
attrs,
120+
template_contents: _,
121+
mathml_annotation_xml_integration_point: _,
122+
} => {
123+
if &name.local == "meta" {
124+
for attr in attrs.borrow().iter() {
125+
if &attr.name.local == "property" {
126+
if attr.value.to_string().eq("og:title") {
127+
return attrs
128+
.borrow()
129+
.iter()
130+
.find(|x| &x.name.local == "content")
131+
.map(|x| x.value.to_string());
132+
}
133+
}
134+
}
135+
}
136+
None
137+
}
138+
_ => None,
139+
})
140+
}
141+
142+
fn get_download_url(addon_url: &str) -> Option<String> {
143+
let re = Regex::new(r"^https://.*esoui.com/downloads/info(\d+)-(.+)\.html$").unwrap();
144+
re.captures(addon_url).map(|captures| {
145+
format!(
146+
"https://www.esoui.com/downloads/download{}-{}.html",
147+
captures[1].to_owned(),
148+
captures[2].to_owned()
149+
)
150+
})
151+
}

0 commit comments

Comments
 (0)