layouts_rs/
lib.rs

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>, // don't merge
30    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        //dbg!(file);
109        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}