1extern crate regex;
2#[macro_use]
3extern crate serde_derive;
4extern crate butlerd;
5extern crate dirs;
6use butlerd::Butler;
7use config::*;
8extern crate serde_json;
9use std::fs::{DirEntry, OpenOptions};
10use std::io::{prelude::*, Read};
11use std::process::Command;
12use std::{env, fmt, fs, io};
13#[derive(Serialize, Deserialize, Debug)]
15pub struct Game {
16 pub command: String,
17 pub pname: String,
18 pub name: String,
19 pub typeg: games::GameType,
20}
21
22pub mod games {
24 use self::GameType::*;
25 use crate::{helper::*, *};
26 #[derive(Serialize, Deserialize, Debug, PartialEq)]
28 #[serde(rename_all = "lowercase")]
29 pub enum GameType {
30 Itch,
31 Steam,
32 Lutris,
33 Exe,
34 Dolphin,
35 WyvernGOG,
36 }
37 impl fmt::Display for GameType {
38 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
39 write!(f, "{:?}", self)
40 }
41 }
42 pub fn check_games() {
44 let games = get_games();
45 for game in games {
46 let m = fs::metadata(gd() + &game);
47 if m.is_ok() {
48 if m.unwrap().is_dir() {
49 let mut command = String::new();
50 fs::File::open(gd() + &game + "/start")
51 .unwrap()
52 .read_to_string(&mut command)
53 .unwrap();
54 let mut commandl = command.lines();
55 commandl.next().unwrap();
56 let mut command = commandl.next().unwrap().to_string();
57 let mut typeg = Exe;
58 if command.contains("steam://rungameid") {
59 typeg = Steam;
60 } else if command.contains("lutris:rungameid") {
61 typeg = Lutris;
62 }
63 let mut games = Game {
64 name: game.clone(),
65 pname: game.clone(),
66 command: command,
67 typeg: typeg,
68 };
69 add_game(games);
70 println!("Converted {}", game);
71 fs::remove_dir_all(gd() + &game).unwrap();
72 }
73 }
74 }
75 }
76 pub fn add_game(game: Game) {
78 if fs::metadata(gd() + &game.name + ".json").is_ok() {
79 println!(" Already made a shortcut for {}", game.pname);
80 } else {
81 let mut ok = true;
82 let blocked = get_config().blocked;
83 for block in blocked {
84 if game.name == block {
85 ok = false;
86 }
87 }
88 if ok {
89 OpenOptions::new()
90 .create(true)
91 .write(true)
92 .open(gd() + &game.name + ".json")
93 .unwrap()
94 .write_all(serde_json::to_string(&game).unwrap().as_bytes())
95 .unwrap();
96 println!(" Made shortcut for {}", game.pname);
97 } else {
98 println!(" {} is in your blocked list", game.pname);
99 }
100 }
101 }
102 pub fn get_games() -> Vec<String> {
104 fs::read_dir(gd())
105 .expect("Can't read in games")
106 .collect::<Vec<io::Result<DirEntry>>>()
107 .into_iter()
108 .map(|entry| {
109 entry
110 .unwrap()
111 .file_name()
112 .into_string()
113 .unwrap()
114 .replace(".json", "")
115 })
116 .collect::<Vec<String>>()
117 }
118 pub fn list_games() {
120 println!("Currently registered games:");
121 let entries = get_games();
122 println!("Name - Procname - Type");
123 for entry in entries {
124 let game = read_game(entry).unwrap();
125 println!("{} - {} - {}", game.pname, game.name, game.typeg);
126 }
127 }
128 pub fn run_game<N>(name: N) -> Result<String, String>
130 where
131 N: Into<String>,
132 {
133 let proced = create_procname(name.into());
134 let g = read_game(proced);
135 if g.is_ok() {
136 let g = g.unwrap();
137 match g.typeg {
138 Itch => {
139 let butler = Butler::new().expect("Has butler been uninstalled?");
140 butler.launch_game(&g.command);
141 return Ok("Launched through butler".to_string());
142 }
143 Dolphin => {
144 let result = Command::new("dolphin-emu-cli")
145 .arg(g.command)
146 .output()
147 .expect("Couldn't run dolphin game");
148 if !result.status.success() {
149 return Err(String::from_utf8_lossy(&result.stderr)
150 .into_owned()
151 .to_string());
152 } else {
153 return Ok(String::from_utf8_lossy(&result.stdout)
154 .into_owned()
155 .to_string());
156 }
157 }
158 WyvernGOG => {
159 let path = std::path::PathBuf::from(&g.command);
160 let start = path.join(std::path::PathBuf::from("start.sh"));
161 let result = Command::new(start.to_str().unwrap())
162 .output()
163 .expect("Couldn't run GOG game!");
164 if !result.status.success() {
165 return Err(String::from_utf8_lossy(&result.stderr)
166 .into_owned()
167 .to_string());
168 } else {
169 return Ok(String::from_utf8_lossy(&result.stdout)
170 .into_owned()
171 .to_string());
172 }
173 }
174 _ => {
175 let result = Command::new("sh")
176 .arg("-c")
177 .arg(g.command)
178 .output()
179 .expect("Couldn't run selected game!");
180 if !result.status.success() {
181 return Err(String::from_utf8_lossy(&result.stderr)
182 .into_owned()
183 .to_string());
184 } else {
185 return Ok(String::from_utf8_lossy(&result.stdout)
186 .into_owned()
187 .to_string());
188 }
189 }
190 }
191 } else {
192 println!("Couldn't find that game installed. Maybe you misspelled something?");
193 Err("Nonexistent".to_string())
194 }
195 }
196 pub fn rm_game<N>(name: N)
198 where
199 N: Into<String>,
200 {
201 let res = fs::remove_file(String::from(gd() + create_procname(name).as_ref()) + ".json");
202 if res.is_ok() {
203 println!("Game removed!");
204 } else {
205 println!("Game did not exist. So, removed?");
206 }
207 }
208 pub fn add_game_p<N>(name: N, exe: N, wine: bool)
210 where
211 N: Into<String>,
212 {
213 let (name, exe) = (name.into(), exe.into());
214 let mut path = env::current_dir().unwrap();
215 path.push(exe.clone());
216 let name = create_procname(name.as_str());
218 let pname = name.clone();
219 if fs::metadata(gd() + &name + ".json").is_ok() {
220 println!("A shortcut has already been made for {}", pname);
221 } else {
222 println!("Creating shortcut for {:?} with a name of {}", path, name);
223 let mut start = String::from("");
224 if wine {
225 let mut winestr = String::from("wine ");
226 if exe.to_lowercase().contains(".lnk") {
227 winestr = winestr + "start ";
228 }
229 start.push_str(&winestr);
230 }
231 let command = String::from(
232 start
233 + &(path
234 .into_os_string()
235 .into_string()
236 .unwrap()
237 .replace(" ", "\\ ")),
238 );
239 let game = Game {
240 pname: pname.to_string(),
241 name: name,
242 command: command,
243 typeg: Exe,
244 };
245 add_game(game);
246 }
247 }
248
249 pub fn read_game<N>(name: N) -> Result<Game, String>
251 where
252 N: Into<String>,
253 {
254 let name = name.into();
255 if fs::metadata(gd() + &name + ".json").is_ok() {
256 let mut stri = String::new();
257 fs::File::open(gd() + &name + ".json")
258 .unwrap()
259 .read_to_string(&mut stri)
260 .unwrap();
261 let g: Game = serde_json::from_str(&stri).unwrap();
262 return Ok(g);
263 }
264 return Err("No such game".to_string());
265 }
266}
267
268pub mod auto {
270 use self::GameType::*;
271 use crate::{games::*, helper::*, *};
272 pub struct SearchResult {
274 pub appid: String,
275 pub name: String,
276 pub outname: String,
277 }
278 pub fn get_lutris() -> Result<Vec<(String, String)>, String> {
280 let games = Command::new("lutris").arg("-l").output();
281 if games.is_ok() {
282 let games = games.unwrap();
283 if games.status.success() {
284 let games_list = String::from_utf8_lossy(&games.stdout);
285 return Ok(games_list
286 .lines()
287 .filter(|x| x.find("wine").is_some())
288 .map(|x| {
289 let n = x.split("|").collect::<Vec<&str>>();
290 (String::from(n[0].trim()), String::from(n[1].trim()))
291 })
292 .collect::<Vec<(String, String)>>());
293 } else {
294 return Err("Lutris not installed".to_string());
295 }
296 } else {
297 return Err("Lutris not installed".to_string());
298 }
299 }
300
301 pub fn update_itch() {
303 if fs::metadata(get_home() + "/.config/itch").is_ok() {
304 let btest = Butler::new();
305 if btest.is_ok() {
306 let mut already = get_games()
307 .iter_mut()
308 .filter(|x| {
309 let read = read_game(x.to_string()).unwrap();
310 read.typeg == Itch
311 })
312 .map(|x| x.to_string())
313 .collect::<Vec<String>>();
314 println!(">> Reading in itch.io games");
315 let butler = btest.expect("Couldn't start butler daemon");
316 let caves = butler.fetchall().expect("Couldn't fetch butler caves");
317 for cave in caves {
318 let game = cave.game;
319 let name = game.title;
320 let procname = create_procname(name.as_str());
321 let g = Game {
322 pname: name,
323 name: procname.clone(),
324 command: cave.id,
325 typeg: Itch,
326 };
327 add_game(g);
328 let mut i = 0;
329 while i < already.len() {
330 if already[i] == procname {
331 already.remove(i);
332 }
333 i += 1;
334 }
335 }
336 for left in already {
337 println!("{} has been uninstalled. Removing from registry.", left);
338 rm_game(left);
339 }
340 } else {
341 println!("Itch.io client not installed!");
342 }
343 } else {
344 println!("Itch.io client not installed!");
345 }
346 }
347 pub fn update_steam(dirs: Vec<String>) {
349 let mut already = get_games();
350 for x in &dirs {
351 println!(">> Reading in steam library {}", &x);
352 let name = x.to_owned();
353 let entries_try = fs::read_dir(name.clone() + "/common");
354 if entries_try.is_ok() {
355 let entries = fs::read_dir(x.to_owned() + "/common")
356 .expect("Can't read in games")
357 .into_iter()
358 .map(|x| proc_path(x.unwrap()));
359 for entry in entries {
360 let results = search_games(entry, x.to_owned());
362 if results.is_some() {
363 let results = results.unwrap();
364 let procname = create_procname(results.name.as_str());
365 let pname = results.name;
366 let command =
367 String::from("steam 'steam://rungameid/") + &results.appid + "'";
368 let game = Game {
369 name: procname.clone(),
370 pname: pname.clone(),
371 command: command,
372 typeg: Steam,
373 };
374 add_game(game);
375 let mut i = 0;
376 while i < already.len() {
377 if already[i] == procname {
378 already.remove(i);
379 }
380 i += 1;
381 }
382 }
383 }
384 } else {
385 println!(
386 "Directory {} does not exist or is not a valid steam library",
387 name
388 );
389 }
390 }
391 for al in already {
392 let typeg = read_game(al.clone()).unwrap().typeg;
393 if typeg == Steam {
394 println!("{} has been uninstalled. Removing game from registry.", al);
395 rm_game(al);
396 }
397 }
398 }
399 pub fn update_lutris() {
401 let lut = get_lutris();
402 if lut.is_err() {
403 println!(">> No wine games found in lutris, or lutris not installed");
404 } else {
405 println!(">> Reading in lutris wine games");
406 for game in lut.unwrap() {
407 let procname = create_procname(game.1.as_str());
408 let pname = game.1.clone();
409 let command = String::from("lutris lutris:rungameid/") + &game.0;
410 let g = Game {
411 pname: pname,
412 name: procname,
413 command: command,
414 typeg: Lutris,
415 };
416 add_game(g);
417 }
418 }
419 }
420 pub fn search_games<N>(rawname: N, steamdir: N) -> Option<SearchResult>
422 where
423 N: Into<String>,
424 {
425 let (rawname, steamdir) = (rawname.into(), steamdir.into());
426 let entries = fs::read_dir(steamdir).expect("Can't read installed steam games");
427 for entry in entries {
428 let entry = entry.unwrap().path();
429 let new_entry = entry.into_os_string().into_string().unwrap();
430 if new_entry.find("appmanifest").is_some() {
431 let mut f = fs::File::open(&new_entry).expect("Couldn't open appmanifest");
432 let mut contents = String::new();
433 f.read_to_string(&mut contents)
434 .expect("Could not read appmanifest");
435 if contents.find("installdir").is_some() {
436 let outname = contents
438 .get(
439 contents
440 .find("installdir")
441 .expect("Couldn't find install dir")
442 + 14
443 ..contents.find("LastUpdated").unwrap() - 4,
444 )
445 .unwrap();
446 if outname == rawname {
447 let appid = contents
448 .get(
449 contents.find("appid").unwrap() + 9
450 ..contents.find("Universe").unwrap() - 4,
451 )
452 .unwrap();
453 let name = contents
454 .get(
455 contents.find("name").unwrap() + 8
456 ..contents.find("StateFlags").unwrap() - 4,
457 )
458 .unwrap();
459 return Some(SearchResult {
460 appid: String::from(appid),
461 name: String::from(name),
462 outname: String::from(outname),
463 });
464 }
465 }
466 }
467 }
468 return None;
470 }
471 pub fn imports<N>(dir: N)
473 where
474 N: Into<String>,
475 {
476 let dir = dir.into();
477 let entries = fs::read_dir(&dir).expect("Can't read in folder of games");
478 println!("Reading in directory: {}", dir);
479 for entry in entries {
480 let entry = proc_path(entry.unwrap());
481 println!("Attempting import on {}", &entry);
482 import(entry.as_str());
483 println!("Finished attempted import on {}", &entry);
484 }
485 }
486 pub fn import<N>(dir: N)
488 where
489 N: Into<String>,
490 {
491 let dir = dir.into();
492 let mut path = env::current_dir().unwrap();
493 let entry_format = &dir.split("/").collect::<Vec<&str>>();
494 let real_dir: String = String::from(entry_format[entry_format.len() - 1]);
495 let procname = create_procname(real_dir);
496 println!("Creating game registry named {}.", procname);
497 path.push(dir.clone());
498 let entries = fs::read_dir(&path).expect("Can't read in game folder");
499 let mut found_game = String::new();
500 for entry in entries {
501 let entry = proc_path(entry.unwrap());
502 let mut found = true;
503 if entry.find(".x86_64").is_some() {
504 println!("Found a unity exe. Assuming it is game");
505 } else if entry.find("start.sh").is_some() {
506 println!("Found a GOG game. Assuming it is game");
507 } else if entry.find("start").is_some() {
508 println!("Found older nicohman game exe. Assuming it is game");
509 } else {
510 found = false;
511 }
512 if found == true {
513 found_game = entry.to_owned();
514 }
515 }
516 if found_game.len() > 0 {
517 add_game_p(
518 procname,
519 path.into_os_string().into_string().unwrap() + "/" + &found_game,
520 false,
521 );
522 } else {
523 println!(
524 "Could not find recognizable game exe. You will have to manually specify using eidolon add [name] [exe]"
525 );
526 }
527 }
528}
529pub mod config {
531 use crate::{helper::*, *};
532 use regex::Regex;
533 #[derive(Serialize, Deserialize, Debug)]
535 pub struct Config {
536 pub steam_dirs: Vec<String>,
537 pub menu_command: String,
538 pub prefix_command: String,
539 #[serde(default = "default_blocked")]
540 pub blocked: Vec<String>,
541 }
542 impl Config {
543 fn default() -> Config {
545 Config {
546 steam_dirs: vec!["$HOME/.local/share/Steam/steamapps".to_string()],
547 menu_command: "rofi -theme sidebar -mesg 'eidolon game:' -p '> ' -dmenu"
548 .to_string(),
549 prefix_command: "".to_string(),
550 blocked: default_blocked(),
551 }
552 }
553 }
554 pub struct OldConfig {
556 pub steam_dirs: Vec<String>,
557 pub menu_command: String,
558 pub prefix_command: String,
559 }
560 fn default_blocked() -> Vec<String> {
561 vec![
562 "steamworks_common_redistributables".to_string(),
563 "proton_3.7".to_string(),
564 "proton_3.7_beta".to_string(),
565 ]
566 }
567 pub fn convert_config() {
569 let old = get_config_old();
570 let conf = Config {
571 steam_dirs: old
572 .steam_dirs
573 .into_iter()
574 .map(|x| String::from(x))
575 .collect::<Vec<String>>(),
576 menu_command: String::from(old.menu_command),
577 prefix_command: String::from(old.prefix_command),
578 blocked: default_blocked(),
579 };
580 OpenOptions::new()
581 .create(true)
582 .write(true)
583 .open(get_home() + "/.config/eidolon/config.json")
584 .unwrap()
585 .write_all(serde_json::to_string(&conf).unwrap().as_bytes())
586 .unwrap();
587 fs::remove_file(get_home() + "/.config/eidolon/config").unwrap();
588 }
589 pub fn get_config() -> Config {
591 let mut conf_s = String::new();
592 fs::File::open(get_home() + "/.config/eidolon/config.json")
593 .expect("Couldn't read config")
594 .read_to_string(&mut conf_s)
595 .unwrap();
596 let mut config: Config = serde_json::from_str(&conf_s).unwrap();
597 let fixed = config.steam_dirs.into_iter();
598 config.steam_dirs = fixed
599 .map(|x| {
600 String::from(
601 x.as_str()
602 .replace("$HOME", &get_home())
603 .replace("~", &get_home()),
604 )
605 })
606 .collect::<Vec<String>>();
607 config
608 }
609 pub fn get_config_old() -> OldConfig {
611 let mut conf = String::new();
612 fs::File::open(get_home() + "/.config/eidolon/config")
613 .expect("Couldn't read config")
614 .read_to_string(&mut conf)
615 .expect("Couldn't read in config");
616 let mut conf = conf.lines();
617 let steam_dirs = conf.next().unwrap();
618 let steam_vec = Regex::new(r"(?:([^\|\s]+)\|)")
619 .expect("Couldn't create regex")
620 .captures_iter(steam_dirs)
621 .map(|x| String::from(x.get(1).unwrap().as_str().replace("$HOME", &get_home())))
622 .collect::<Vec<String>>();
623 let menu_command_base = String::from(conf.next().unwrap());
624 let prefix_command_bool = conf.next();
625 let mut prefix_command: &str;
626 if prefix_command_bool.is_some() {
627 prefix_command = prefix_command_bool.unwrap();
628 prefix_command = prefix_command.split('|').collect::<Vec<&str>>()[1];
629 } else {
630 prefix_command = " "
631 }
632 let menu_command = menu_command_base.split('|').collect::<Vec<&str>>()[1];
633 OldConfig {
634 steam_dirs: steam_vec,
635 menu_command: String::from(menu_command),
636 prefix_command: String::from(prefix_command),
637 }
638 }
639 pub fn init() {
641 println!("Beginning config init");
642 if fs::metadata(get_home() + "/.config").is_err() {
643 fs::create_dir(get_home() + "/.config").expect("Couldn't create config directory");
644 }
645 fs::create_dir(get_home() + "/.config/eidolon").expect("Couldn't create eidolon directory");
646 fs::create_dir(get_home() + "/.config/eidolon/games")
647 .expect("Couldn't create games directory");
648 let mut file = OpenOptions::new()
649 .create(true)
650 .write(true)
651 .open(get_home() + "/.config/eidolon/config.json")
652 .unwrap();
653 file.write_all(
654 serde_json::to_string(&Config::default())
655 .unwrap()
656 .as_bytes(),
657 )
658 .unwrap();
659 println!("Correctly initialized base config.");
660 }
661 pub fn startup() -> bool {
663 if check_inited() {
664 true
665 } else {
666 init();
667 check_inited()
668 }
669 }
670 pub fn check_inited() -> bool {
672 if fs::metadata(get_home() + "/.config/eidolon").is_err() || fs::metadata(gd()).is_err() {
673 false
674 } else {
675 if fs::metadata(get_home() + "/.config/eidolon/config").is_ok() {
676 convert_config();
677 true
678 } else if fs::metadata(get_home() + "/.config/eidolon/config.json").is_ok() {
679 true
680 } else {
681 false
682 }
683 }
684 }
685 pub fn gd() -> String {
687 return get_home() + "/.config/eidolon/games/";
688 }
689}
690pub mod helper {
692 use regex::Regex;
693 use std::env;
694 use std::fs::DirEntry;
695 pub fn create_procname<N>(rawname: N) -> String
697 where
698 N: Into<String>,
699 {
700 let mut basename = String::from(rawname.into()).to_lowercase();
701 basename = String::from(basename.trim());
702 let reg_white = Regex::new(r"-|\s").unwrap();
703 let reg_special = Regex::new(r"'|™|:").unwrap();
704 let white_formatted = reg_white.replace_all(&basename, "_");
705 let total_formatted = reg_special.replace_all(&white_formatted, "");
706 return String::from(total_formatted);
707 }
708
709 pub fn proc_path(path: DirEntry) -> String {
711 let base = path.file_name().into_string().unwrap();
712 return base;
713 }
714 pub fn get_home() -> String {
716 return String::from(dirs::home_dir().unwrap().to_str().unwrap());
717 }
718}