1pub mod github;
2
3#[macro_use]
4extern crate derive_object_merge;
5
6use github::GithubClient;
7use glob::glob;
8use object_merge::Merge;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::fs;
12use std::path::PathBuf;
13use std::str::FromStr;
14
15#[derive(Debug, Default, Clone, Serialize, Deserialize)]
16#[serde(default)]
17pub struct Header {
18 pub name: Vec<String>,
19 pub authors: Vec<String>,
20 pub notes: Vec<String>,
21 pub locale: Vec<String>,
22}
23
24#[derive(Debug, Default, Clone, Serialize, Deserialize, Merge)]
25#[serde(default)]
26pub struct Layout {
27 #[shallow_merge]
28 #[serde(flatten)]
29 pub header: Option<Header>, pub hid_locale: Option<String>,
31 pub parent: Option<String>,
32 #[combine]
33 pub locale_notes: Vec<String>,
34 pub to_hid_locale: HashMap<String, String>,
35 pub from_hid_locale: HashMap<String, String>,
36 #[combine]
37 pub keyboard_notes: Vec<String>,
38 pub to_hid_keyboard: HashMap<String, String>,
39 pub from_hid_keyboard: HashMap<String, String>,
40 #[combine]
41 pub led_notes: Vec<String>,
42 pub to_hid_led: HashMap<String, String>,
43 pub from_hid_led: HashMap<String, String>,
44 #[combine]
45 pub sysctrl_notes: Vec<String>,
46 pub to_hid_sysctrl: HashMap<String, String>,
47 pub from_hid_sysctrl: HashMap<String, String>,
48 #[combine]
49 pub consumer_notes: Vec<String>,
50 pub to_hid_consumer: HashMap<String, String>,
51 pub from_hid_consumer: HashMap<String, String>,
52 #[combine]
53 pub composition: HashMap<String, Vec<Vec<String>>>,
54}
55
56impl Layout {
57 pub fn from_file(file: PathBuf) -> Layout {
58 let data = fs::read_to_string(&file).unwrap();
59 Layout::from_str(&data).unwrap()
60 }
61}
62
63impl FromStr for Layout {
64 type Err = serde_json::error::Error;
65
66 fn from_str(s: &str) -> Result<Self, Self::Err> {
67 serde_json::from_str(s)
68 }
69}
70
71#[derive(Debug, Clone)]
72pub enum LayoutSource {
73 Directory(PathBuf),
74 Github(GithubClient, String),
75}
76
77pub fn list_dir(layout_dir: &str) -> Vec<String> {
78 glob(&format!("{}/**/*.json", layout_dir))
79 .unwrap()
80 .filter_map(|path| path.ok())
81 .map(|path| {
82 path.strip_prefix(layout_dir)
83 .unwrap()
84 .to_string_lossy()
85 .to_string()
86 })
87 .collect()
88}
89
90pub fn list_github(client: &GithubClient, reftag: &str) -> Vec<String> {
91 client
92 .list_files(reftag)
93 .unwrap()
94 .into_iter()
95 .filter(|path| path.ends_with(".json"))
96 .collect()
97}
98
99impl LayoutSource {
100 pub fn list_layouts(&self) -> Vec<String> {
101 match self {
102 LayoutSource::Directory(dir) => list_dir(&dir.to_string_lossy()),
103 LayoutSource::Github(client, reftag) => list_github(client, reftag),
104 }
105 }
106
107 pub fn fetch_layout(&self, file: &str) -> Layout {
108 match self {
110 LayoutSource::Directory(dir) => Layout::from_file(dir.join(file)),
111 LayoutSource::Github(client, reftag) => {
112 let data = client.get_file_raw(file, reftag).unwrap();
113 Layout::from_str(&data).unwrap()
114 }
115 }
116 }
117}
118
119#[derive(Debug, Clone)]
120pub struct Layouts {
121 files: Vec<String>,
122 cached_layouts: HashMap<String, Layout>,
123 source: LayoutSource,
124}
125
126impl Layouts {
127 pub fn new(source: LayoutSource) -> Self {
128 Layouts {
129 source,
130 files: vec![],
131 cached_layouts: HashMap::new(),
132 }
133 }
134
135 pub fn from_dir(dir: PathBuf) -> Self {
136 Layouts::new(LayoutSource::Directory(dir))
137 }
138
139 pub fn from_github(repo: String, reftag: String, api_token: Option<String>) -> Self {
140 let client = GithubClient::new(repo, api_token);
141 Layouts::new(LayoutSource::Github(client, reftag))
142 }
143
144 pub fn list_layouts(&mut self) -> Vec<String> {
145 if self.files.is_empty() {
146 self.files = self.source.list_layouts();
147 }
148 self.files.clone()
149 }
150
151 pub fn get_layout(&mut self, name: &str) -> Layout {
152 if !self.cached_layouts.contains_key(name) {
153 let layout = self.source.fetch_layout(name);
154 self.cached_layouts.insert(name.to_string(), layout);
155 }
156
157 let layout = &self.cached_layouts[name];
158 let parent = layout.parent.clone();
159
160 let mut new_layout = layout.clone();
161 if let Some(parent) = parent {
162 new_layout.merge(&self.get_layout(&parent));
163 }
164 new_layout
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::Layouts;
171 use std::path::PathBuf;
172
173 #[test]
174 fn test_dir() {
175 let layout_dir = PathBuf::from("layouts");
176 let mut layouts = Layouts::from_dir(layout_dir);
177 for layout in layouts.list_layouts() {
178 println!("{}:\n{:?}\n", layout, layouts.get_layout(&layout));
179 }
180 }
181
182 #[test]
183 fn test_github() {
184 let api_token = std::env::var("GITHUB_API_TOKEN");
185 let mut layouts = Layouts::from_github(
186 "hid-io/layouts".to_string(),
187 "master".to_string(),
188 api_token.ok(),
189 );
190 for layout in layouts.list_layouts() {
191 println!("{}:\n{:?}\n", layout, layouts.get_layout(&layout));
192 }
193 }
194}