flashed 0.10.1

A flashcard TUI
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::HashSet;
use std::io;
use std::mem;
use std::path::Path;
use std::path::PathBuf;
use std::time::Duration;

use chrono::Local;
use chrono::{DateTime, Utc};

use crate::card::Card;
use crate::scores::Scores;
use crate::Deck;
use crate::Opts;

/// A list of durations which cards will be waiting by.
pub static DURATIONS: [Duration; 5] = [
    Duration::from_secs(0),                     // Instant
    Duration::from_secs(60 * 60 * 24),          // Daily
    Duration::from_secs(60 * 60 * 24 * 7),      // Weekly
    Duration::from_secs(60 * 60 * 24 * 28),     // Monthly
    Duration::from_secs(60 * 60 * 24 * 28 * 2), // Bimonthly
];

/// A convenience type representing an index of [`App::cards`]
pub type CardIdx = usize;

/// App holds the state of the application
#[derive(Debug, Clone)]
pub struct App {
    /// The index of the current card
    card: CardIdx,
    /// The index of the current card in `playable_cards`
    playable_card_pos: CardIdx,
    /// The deck
    deck: Deck,
    /// The list of cards needed to be played
    playable_cards: Vec<CardIdx>,
    /// The list of cards in circulation
    ///
    /// This will usually be the size of the subset deck, and means that cards given incorrect responses are repeated sooner
    circulating: Vec<CardIdx>,
    /// This determines whether the card should show the
    /// question or the answer.
    pub flipped: bool,
    /// The (command line) options of the [`App`] to determine
    /// how it willl run.
    pub opts: Opts,
    /// The currently entered text for Input questions.
    /// Reset when moving to a new questions.
    pub text: String,
}

impl App {
    /// Creates a new app from a deck of cards
    pub fn new(mut deck: Deck, opts: Opts) -> Self {
        deck.cards.sort();
        deck.cards.dedup();

        let mut app = Self {
            card: 0,
            playable_card_pos: 0,
            playable_cards: (0..deck.cards.len()).collect(),
            deck,
            circulating: Vec::new(),
            flipped: false,
            text: String::new(),
            opts,
        };

        app.shuffle_all();
        app.retain_undue();
        app.next_card();

        app
    }

    /// Put the playable cards in order based of their score
    pub fn order_playables(&mut self) {
        self.playable_cards.sort_unstable_by_key(|card| {
            (
                self.circulating.contains(card),
                self.deck.cards[*card].score,
            )
        });
    }

    /// Returns an immutable copy of all the cards
    pub fn cards(&self) -> &[Card] {
        &self.deck.cards
    }

    /// Returns in immutable copy of when different cards
    /// are due.
    ///
    /// The key is garunteed to probably be an index from `[Self::cards]`
    pub fn dues(&self) -> HashMap<CardIdx, Option<DateTime<Utc>>> {
        self.deck.dues()
    }

    /// Returns in immutable copy of each cards scores.
    ///
    /// The key is garunteed to probably be an index from `[Self::cards]`
    pub fn scores(&self) -> HashMap<usize, usize> {
        self.deck.scores()
    }

    /// Returns a copy of cards which are mandatorily in
    /// circulation until answered correctly
    pub fn circulating(&self) -> &[CardIdx] {
        &self.circulating
    }

    /// Flips the current card to show the other side.
    pub fn flip(&mut self) {
        self.flipped ^= true;
    }

    /// Gets the range of cards currently being played with.
    pub fn playable_range(&self) -> usize {
        let range = self
            .opts
            .testing
            .unwrap_or(self.playable_cards.len())
            .min(self.playable_cards.len());
        if range == 0 {
            self.playable_cards.len()
        } else {
            range
        }
    }

    /// Shuffles the deck so you can play with different cards
    pub fn shuffle(&mut self) {
        let range = 0..self.playable_range();
        fastrand::shuffle(&mut self.playable_cards[range]);
    }

    /// Shuffles the whole deck so you can play with different cards
    pub fn shuffle_all(&mut self) {
        fastrand::shuffle(&mut self.playable_cards);
    }

    /// Removes any cards which aren't due from the deck
    pub fn retain_undue(&mut self) {
        // Take the memory because otherwise we will have borrowed
        // self twice.
        let mut playable_cards = mem::take(&mut self.playable_cards);

        // Remove all the cards which we don't need
        playable_cards.retain(|x| !self.current_card_is_finished(*x));

        // Add it back into self
        self.playable_cards = playable_cards;

        // Make sure our cursor is in a valid place
        if !self.playable_cards.contains(&self.card) {
            self.card = *self.playable_cards.first().unwrap_or(&0);
        }
    }

    /// Moves on to the next card
    pub fn next_card(&mut self) {
        self.retain_undue();
        if !self.playable_cards.is_empty() {
            self.playable_card_pos += 1;
            self.playable_card_pos %= self.playable_range();
            self.card = self.playable_cards[self.playable_card_pos];
            self.flipped = false;
            if self.playable_card_pos == 0 {
                self.shuffle();
            }
        }
        if !self.text.is_empty() {
            self.text = String::new();
        }
    }

    /// Gets the current card from the app.
    pub fn card(&self) -> &Card {
        &self.deck.cards[self.card]
    }

    /// Gets the current card from the app.
    pub fn card_mut(&mut self) -> &mut Card {
        &mut self.deck.cards[self.card]
    }

    /// Gets the score of the current card from the app.
    pub fn card_score(&self) -> usize {
        self.deck.cards[self.card].score
    }

    /// Resets the duration of the current card.
    ///
    /// This will index [`DURATIONS`] and work out from that
    /// how far forth it should be due.
    pub fn reset_current_card_duration(&mut self) {
        let score = self.card_score().min(DURATIONS.len());
        let duration = DURATIONS[score.min(4)];
        let duration = chrono::Duration::from_std(duration);
        let duration = duration.unwrap_or_else(|_| chrono::Duration::max_value());
        let val = Utc::now() + duration;
        self.card_mut().due_date = Some(val);
    }

    /// Changes the score of the current card.
    ///
    /// A card can not have a negative score. If the
    /// `zeroize` option is true then a negative `val`
    /// will set the score to zero.
    pub fn change_current_card_score(&mut self, val: isize) {
        let z = self.opts.zeroize;
        let card = self.card_mut();
        if val > 0 {
            card.score = card.score.saturating_add(val.try_into().unwrap_or(0));
            if let Some(x) = self.circulating.iter().position(|x| *x == self.card) {
                self.circulating.swap_remove(x);
            }
        } else if z {
            card.score = 0;
        } else {
            card.score = card.score.saturating_sub((0 - val).try_into().unwrap_or(0));
            if !self.circulating.contains(&self.card) {
                self.circulating.push(self.card)
            }
        }
        self.reset_current_card_duration();
    }

    /// Determines if the current card still should be played
    /// or if its due date has expired.
    pub fn current_card_is_finished(&self, card: usize) -> bool {
        let now = Utc::now();

        if self.circulating.contains(&card) {
            false
        } else if let Some(val) = self.deck.cards[card].due_date {
            now.with_timezone(&Local).date() < val.with_timezone(&Local).date()
        } else {
            false
        }
    }

    /// Gets how many cards still need to be played.
    pub fn unfinished_count(&self) -> usize {
        let mut count = 0;

        for card in 0..self.deck.cards.len() {
            if !self.current_card_is_finished(card) {
                count += 1;
            }
        }

        count
    }

    /// Writes the scores recursively to the relevant files, as specified in [`App::opts`].
    #[cfg(feature = "serde_json")]
    pub fn write_scores(&self) -> io::Result<()> {
        if !self.opts.nowrite {
            for path in self.get_paths() {
                println!("Saving scores for {}", path.display());
                self.write_score_path(path)?;
            }
            // let new_path = self.opts.input.with_extension("score.json");
            // let file = File::create(new_path)?;

            // let scores: Scores = self.into();
            // serde_json::to_writer(&file, &scores)?;
        }

        Ok(())
    }

    /// Adds a [`flashed::Scores`] into an [`App`]
    pub fn add_scores(&mut self, scores: Scores, path: &Path) {
        for (card, score) in scores.scores {
            let pos = self.cards().iter().position(|x| x.inner == card);
            if let Some(pos) = pos {
                self.deck.cards[pos].score = score;
            }
        }

        for (card, due) in scores.dues {
            let pos = self
                .cards()
                .iter()
                .position(|x| x.inner == card && x.path == path);
            if let Some(pos) = pos {
                self.deck.cards[pos].due_date = Some(due)
            }
        }

        for card in scores.circulating {
            let pos = self.cards().iter().position(|x| x.inner == card);
            if let Some(pos) = pos {
                self.circulating.push(pos);
            }
        }
    }

    /// Reads and applies the scores from the file or directory, as specified in [`App::opts`]
    #[cfg(feature = "serde_json")]
    pub fn read_scores(&mut self) -> io::Result<()> {
        self.read_scores_dir(self.opts.input.clone())
    }

    /// Recursively searches a directory and applies all scores
    #[cfg(feature = "serde_json")]
    fn read_scores_dir(&mut self, path: PathBuf) -> io::Result<()> {
        use std::fs::{self, File};
        if path.is_dir() {
            for child in fs::read_dir(path)? {
                self.read_scores_dir(child?.path())?
            }
            Ok(())
        } else {
            let new_path = path.with_extension("score.json");
            match File::open(&new_path) {
                Ok(file) => {
                    if let Ok(scores) = serde_json::from_reader(&file) {
                        self.add_scores(scores, &path);
                    };
                    Ok(())
                }
                Err(e) if e.kind() == io::ErrorKind::NotFound => Ok(()),
                Err(e) => Err(e),
            }
        }
    }

    /// Returns a list of all card paths in the deck
    pub fn get_paths(&self) -> Vec<&PathBuf> {
        let mut ds = HashSet::new();
        for card in self.cards() {
            ds.insert(&card.path);
        }
        ds.into_iter().collect()
    }

    /// Writes the scores of a single file's worth of cards to the relevant path
    #[cfg(feature = "serde_json")]
    fn write_score_path(&self, path: &Path) -> io::Result<()> {
        use std::fs::File;

        let new_path = path.with_extension("score.json");

        match File::create(&new_path) {
            Ok(file) => {
                let new_scores = self.scores_from_path(path);
                serde_json::to_writer(&file, &new_scores)?;
                Ok(())
            }
            Err(e) if e.kind() == io::ErrorKind::NotFound => {
                let file = File::create(new_path)?;
                let new_scores = self.scores_from_path(path);
                serde_json::to_writer(&file, &new_scores)?;
                Ok(())
            }
            Err(e) => Err(e),
        }?;

        Ok(())
    }

    /// Gets the scores for all cards with the given path
    pub fn scores_from_path(&self, path: &Path) -> Scores {
        Scores {
            scores: {
                let mut scores = Vec::new();

                for card in self.cards().iter().filter(|c| c.path == path) {
                    scores.push((Cow::Borrowed(card).inner.clone(), card.score));
                }

                scores
            },
            dues: {
                let mut dues = Vec::new();

                // Loops through all the dues and references
                // the card (if it exists) into the hashmap.
                for card in self.cards().iter().filter(|c| c.path == path) {
                    dues.push((
                        Cow::Borrowed(card).inner.clone(),
                        card.due_date.unwrap_or(Utc::now()),
                    ));
                }

                dues
            },
            circulating: {
                let mut circulating = Vec::new();
                for card in self.circulating().iter().filter(|c| {
                    if let Some(card) = self.cards().get(**c) {
                        card.path == path
                    } else {
                        false
                    }
                }) {
                    if let Some(card) = self.cards().get(*card) {
                        circulating.push(Cow::Borrowed(card).inner.clone())
                    }
                }
                circulating
            },
        }
    }
}