Skip to content

Commit 1b057b1

Browse files
authored
Add switchable themes with T keybinding (#8)
1 parent c6dde9f commit 1b057b1

5 files changed

Lines changed: 269 additions & 90 deletions

File tree

src/lib.rs

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ use ratatui::{
1919

2020
use crate::pomodoro::{Mode, Pomodoro};
2121
use crate::radio::Radio;
22+
use crate::storage::Config;
2223
use crate::theme::Theme;
2324
use crate::todo::TodoList;
2425
use crate::ui::logo::{self, LOGO_HEIGHT, LOGO_WIDTH};
@@ -57,14 +58,18 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
5758
let mut todos = TodoList::load();
5859
let mut last_second = Instant::now();
5960

61+
let config = storage::load_config();
62+
let mut theme_name = config.theme;
63+
let mut theme = Theme::from_name(theme_name);
64+
6065
loop {
6166
terminal.draw(|f| {
6267
let area = f.area();
6368
f.render_widget(ratatui::widgets::Clear, area);
6469

6570
let block = Block::default()
66-
.style(Theme::base())
67-
.border_style(Theme::frame())
71+
.style(theme.base())
72+
.border_style(theme.frame())
6873
.borders(Borders::ALL);
6974

7075
f.render_widget(block, area);
@@ -85,13 +90,13 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
8590
2 => ".. ",
8691
_ => "...",
8792
};
88-
("◌", Theme::frame(), format!(" loading{}", dots))
93+
("◌", theme.frame(), format!(" loading{}", dots))
8994
} else if radio.is_playing() {
90-
("♫", Theme::accent(), String::new())
95+
("♫", theme.accent(), String::new())
9196
} else if radio.is_error() {
92-
("✗", Theme::hot(), " error".to_string())
97+
("✗", theme.hot(), " error".to_string())
9398
} else {
94-
("♪", Theme::frame(), String::new())
99+
("♪", theme.frame(), String::new())
95100
};
96101

97102
let station_area = Rect {
@@ -103,10 +108,10 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
103108

104109
let station_line = Paragraph::new(Line::from(vec![
105110
Span::styled(format!("{} ", radio_icon), radio_style),
106-
Span::styled(radio.station().name, Theme::hot()),
107-
Span::styled(status_text, Theme::frame()),
111+
Span::styled(radio.station().name, theme.hot()),
112+
Span::styled(status_text, theme.frame()),
108113
]))
109-
.style(Theme::base());
114+
.style(theme.base());
110115

111116
// Pomodoro display (top right)
112117
let pomo_width: u16 = 16;
@@ -133,20 +138,22 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
133138
};
134139

135140
let help = Paragraph::new(Line::from(vec![
136-
Span::styled("q ", Theme::accent()),
137-
Span::styled("quit ", Theme::frame()),
138-
Span::styled("s ", Theme::accent()),
139-
Span::styled(format!("{} ", radio_action), Theme::frame()),
140-
Span::styled("←/→ ", Theme::accent()),
141-
Span::styled("station ", Theme::frame()),
142-
Span::styled("t ", Theme::accent()),
143-
Span::styled("todos ", Theme::frame()),
144-
Span::styled("p ", Theme::accent()),
145-
Span::styled("pomo ", Theme::frame()),
146-
Span::styled("space ", Theme::accent()),
147-
Span::styled(pomo_action, Theme::frame()),
141+
Span::styled("q ", theme.accent()),
142+
Span::styled("quit ", theme.frame()),
143+
Span::styled("s ", theme.accent()),
144+
Span::styled(format!("{} ", radio_action), theme.frame()),
145+
Span::styled("←/→ ", theme.accent()),
146+
Span::styled("station ", theme.frame()),
147+
Span::styled("t ", theme.accent()),
148+
Span::styled("todos ", theme.frame()),
149+
Span::styled("T ", theme.accent()),
150+
Span::styled("theme ", theme.frame()),
151+
Span::styled("p ", theme.accent()),
152+
Span::styled("pomo ", theme.frame()),
153+
Span::styled("space ", theme.accent()),
154+
Span::styled(pomo_action, theme.frame()),
148155
]))
149-
.style(Theme::base());
156+
.style(theme.base());
150157

151158
// Todo list in center area
152159
let todo_area = Rect {
@@ -156,10 +163,10 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
156163
height: area.height.saturating_sub(8),
157164
};
158165

159-
f.render_widget(logo::logo(), logo_area);
166+
f.render_widget(logo::logo(&theme, theme_name), logo_area);
160167
f.render_widget(station_line, station_area);
161168
f.render_widget(help, help_area);
162-
todos.draw(f, todo_area);
169+
todos.draw(f, todo_area, &theme);
163170

164171
// Pomodoro on top
165172
if pomo.visible {
@@ -170,20 +177,20 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
170177
};
171178
let status = if pomo.running { "▶" } else { "⏸" };
172179
let timer_style = if pomo.running {
173-
Theme::accent()
180+
theme.accent()
174181
} else {
175-
Theme::frame()
182+
theme.frame()
176183
};
177184

178185
let pomo_widget = Paragraph::new(vec![
179-
Line::from(Span::styled(format!("{:02}:{:02}", mm, ss), Theme::hot())),
186+
Line::from(Span::styled(format!("{:02}:{:02}", mm, ss), theme.hot())),
180187
Line::from(vec![
181188
Span::styled(format!("{} ", status), timer_style),
182-
Span::styled(mode_label, Theme::title()),
189+
Span::styled(mode_label, theme.title()),
183190
]),
184191
])
185192
.alignment(Alignment::Right)
186-
.style(Theme::base());
193+
.style(theme.base());
187194

188195
f.render_widget(pomo_widget, pomo_area);
189196
}
@@ -215,6 +222,11 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
215222
return Ok(());
216223
}
217224
KeyCode::Char('t') => todos.toggle_visible(),
225+
KeyCode::Char('T') => {
226+
theme_name = theme_name.next();
227+
theme = Theme::from_name(theme_name);
228+
storage::save_config(&Config { theme: theme_name });
229+
}
218230
KeyCode::Char('n') => todos.enter_input_mode(),
219231
KeyCode::Char('j') => todos.move_down(),
220232
KeyCode::Char('k') => todos.move_up(),
@@ -245,6 +257,11 @@ fn run_app(terminal: &mut Terminal<CrosstermBackend<Stdout>>) -> Result<()> {
245257
return Ok(());
246258
}
247259
KeyCode::Char('t') => todos.toggle_visible(),
260+
KeyCode::Char('T') => {
261+
theme_name = theme_name.next();
262+
theme = Theme::from_name(theme_name);
263+
storage::save_config(&Config { theme: theme_name });
264+
}
248265
KeyCode::Char('p') => pomo.toggle_visible(),
249266
KeyCode::Char('s') => radio.toggle(),
250267
KeyCode::Left => radio.prev_station(),

src/storage.rs

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::path::PathBuf;
33

44
use serde::{Deserialize, Serialize};
55

6+
use crate::theme::ThemeName;
67
use crate::todo::Task;
78

89
#[derive(Serialize, Deserialize, Default)]
@@ -11,9 +12,31 @@ pub struct TaskData {
1112
pub next_id: u64,
1213
}
1314

14-
pub fn get_data_path() -> PathBuf {
15+
#[derive(Serialize, Deserialize)]
16+
pub struct Config {
17+
#[serde(default)]
18+
pub theme: ThemeName,
19+
}
20+
21+
impl Default for Config {
22+
fn default() -> Self {
23+
Self {
24+
theme: ThemeName::default(),
25+
}
26+
}
27+
}
28+
29+
fn data_dir() -> PathBuf {
1530
let base = dirs::data_dir().unwrap_or_else(|| PathBuf::from("."));
16-
base.join("loshell").join("tasks.json")
31+
base.join("loshell")
32+
}
33+
34+
pub fn get_data_path() -> PathBuf {
35+
data_dir().join("tasks.json")
36+
}
37+
38+
fn config_path() -> PathBuf {
39+
data_dir().join("config.json")
1740
}
1841

1942
pub fn load_tasks() -> TaskData {
@@ -33,3 +56,21 @@ pub fn save_tasks(data: &TaskData) {
3356
let _ = fs::write(&path, json);
3457
}
3558
}
59+
60+
pub fn load_config() -> Config {
61+
let path = config_path();
62+
match fs::read_to_string(&path) {
63+
Ok(content) => serde_json::from_str(&content).unwrap_or_default(),
64+
Err(_) => Config::default(),
65+
}
66+
}
67+
68+
pub fn save_config(config: &Config) {
69+
let path = config_path();
70+
if let Some(parent) = path.parent() {
71+
let _ = fs::create_dir_all(parent);
72+
}
73+
if let Ok(json) = serde_json::to_string_pretty(config) {
74+
let _ = fs::write(&path, json);
75+
}
76+
}

0 commit comments

Comments
 (0)