Skip to content

Commit 77dca15

Browse files
authored
Merge pull request #3 from xRipzch/feature/default-project
Add default project support with visual indicators and deletion safety Added themes.
2 parents edd6751 + d75e88a commit 77dca15

7 files changed

Lines changed: 593 additions & 59 deletions

File tree

README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ tui-kanban
8787
- **Enter** - Select project
8888
- **a** - Add new project
8989
- **d** - Delete project
90+
- **s** - Set selected project as default
9091
- **Esc** - Close project list
9192

9293
### Tags
@@ -114,10 +115,64 @@ Projects and tasks are automatically saved to:
114115

115116
If you're migrating from an older version, your data will be automatically migrated from the old location.
116117

118+
## Default Projects
119+
120+
TUI-Kanban supports setting a default project that opens automatically when you launch the application. There are two ways to set a default project:
121+
122+
### Global Default (Recommended for most users)
123+
124+
Set a project as default globally using the project list (Ctrl+P):
125+
1. Press **Ctrl+P** to open the project list
126+
2. Navigate to your desired project using **j/k** or arrow keys
127+
3. Press **s** to set it as the default project
128+
129+
Your choice is saved to `~/.config/tui-kanban/config.json` and will apply everywhere.
130+
131+
### Directory-Specific Default
132+
133+
For advanced workflows where you work on multiple projects in different directories, you can create a `.tui-kanban-project` file in any directory:
134+
135+
```bash
136+
# In your project directory
137+
echo "MyProject" > .tui-kanban-project
138+
```
139+
140+
When you run `tui-kanban` from that directory, it will automatically open "MyProject".
141+
142+
**Priority order:**
143+
1. Directory-specific `.tui-kanban-project` file (if present in current directory)
144+
2. Global default from `config.json` (set via 's' in project list)
145+
3. First project in the list (default behavior)
146+
117147

118148
https://github.com/user-attachments/assets/fa467298-e3c5-4770-b4b5-c40280f6f9ab
119149

150+
## Themes
151+
152+
TUI-Kanban supports multiple color themes to match your terminal preferences and improve readability. You can change the theme by editing your config file.
153+
154+
### Available Themes
155+
156+
- **high-contrast** (Default) - Bright, bold colors for maximum visibility on any terminal theme
157+
- **classic** - Traditional color scheme with improved contrast over the original
158+
- **solarized-dark** - Popular Solarized palette designed for reduced eye strain
159+
- **gruvbox** - Warm, retro color scheme with earthy tones
160+
- **nord** - Cool, arctic-inspired colors from the Nord palette
161+
162+
### Changing Your Theme
163+
164+
Edit your configuration file at `~/.config/tui-kanban/config.json` (Linux/macOS) or `%APPDATA%\tui-kanban\config.json` (Windows):
165+
166+
```json
167+
{
168+
"theme": "nord",
169+
"default_project": "Work"
170+
}
171+
```
172+
173+
Available theme names: `high-contrast`, `classic`, `solarized-dark`, `gruvbox`, `nord`
120174

175+
Changes take effect the next time you launch tui-kanban. If the theme name is invalid or not specified, it will default to `high-contrast`.
121176

122177
## Contributors
123178

src/app.rs

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::board::{Board, BoardColumn, Project, Task};
22
use crate::storage;
3+
use crate::theme::Theme;
34

45
// application state
56
pub struct App {
@@ -15,6 +16,7 @@ pub struct App {
1516
pub input_buffer: String,
1617
pub focused_field: TaskField,
1718
pub disable_saving: bool, // For testing
19+
pub theme: Theme,
1820
}
1921

2022
// which field is focused in task detail view
@@ -39,15 +41,29 @@ pub enum InputMode {
3941
AddingProject,
4042
AddingColumn,
4143
RenamingColumn,
44+
ConfirmingDelete,
4245
}
4346

4447
impl App {
4548
// create new app state
4649
pub fn new() -> Self {
50+
let projects = storage::load_projects();
51+
let config = storage::load_config();
52+
53+
// Determine which project to start with
54+
let current_project = Self::determine_initial_project(&projects, &config);
55+
56+
// Load theme from config
57+
let theme = config
58+
.theme
59+
.as_ref()
60+
.and_then(|name| Theme::from_name(name))
61+
.unwrap_or_default();
62+
4763
Self {
48-
projects: storage::load_projects(),
49-
current_project: 0,
50-
selected_project_index: 0,
64+
projects,
65+
current_project,
66+
selected_project_index: current_project,
5167
selected_column: 0, // Default to the first column
5268
selected_index: 0,
5369
scroll_offset: 0,
@@ -57,7 +73,31 @@ impl App {
5773
input_buffer: String::new(),
5874
focused_field: TaskField::Title,
5975
disable_saving: false,
76+
theme,
77+
}
78+
}
79+
80+
// Determine which project to start with based on priority:
81+
// 1. Directory-specific .tui-kanban-project file
82+
// 2. Global default from config.json
83+
// 3. First project (index 0)
84+
fn determine_initial_project(projects: &[Project], config: &storage::Config) -> usize {
85+
// Priority 1: Check for directory-specific project file
86+
if let Some(dir_project_name) = storage::get_directory_project() {
87+
if let Some(index) = projects.iter().position(|p| p.name == dir_project_name) {
88+
return index;
89+
}
90+
}
91+
92+
// Priority 2: Check config for default project
93+
if let Some(default_name) = &config.default_project {
94+
if let Some(index) = projects.iter().position(|p| p.name == *default_name) {
95+
return index;
96+
}
6097
}
98+
99+
// Priority 3: Default to first project
100+
0
61101
}
62102

63103
pub fn new_with_projects(projects: Vec<Project>) -> Self {
@@ -74,6 +114,7 @@ impl App {
74114
input_buffer: String::new(),
75115
focused_field: TaskField::Title,
76116
disable_saving: true,
117+
theme: Theme::default(),
77118
}
78119
}
79120

@@ -425,7 +466,8 @@ impl App {
425466
InputMode::Normal
426467
| InputMode::ViewingTask
427468
| InputMode::ViewingHelp
428-
| InputMode::ProjectList => {}
469+
| InputMode::ProjectList
470+
| InputMode::ConfirmingDelete => {}
429471
}
430472
self.cancel_input();
431473
}
@@ -515,7 +557,13 @@ impl App {
515557
self.input_buffer.clear();
516558
}
517559

518-
pub fn delete_project(&mut self) {
560+
pub fn start_confirming_delete(&mut self) {
561+
if self.projects.len() > 1 {
562+
self.input_mode = InputMode::ConfirmingDelete;
563+
}
564+
}
565+
566+
pub fn confirm_delete_project(&mut self) {
519567
if self.projects.len() > 1 {
520568
self.projects.remove(self.selected_project_index);
521569
if self.selected_project_index >= self.projects.len() {
@@ -526,6 +574,18 @@ impl App {
526574
}
527575
self.save();
528576
}
577+
self.input_mode = InputMode::ProjectList;
578+
}
579+
580+
pub fn cancel_delete(&mut self) {
581+
self.input_mode = InputMode::ProjectList;
582+
}
583+
584+
pub fn set_project_as_default(&mut self) {
585+
let project_name = self.projects[self.selected_project_index].name.clone();
586+
let mut config = storage::load_config();
587+
config.default_project = Some(project_name);
588+
let _ = storage::save_config(&config);
529589
}
530590

531591
// show help view

src/board.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ impl Project {
2323
board: Board::new(),
2424
}
2525
}
26+
27+
// Count total tasks in this project
28+
pub fn count_tasks(&self) -> usize {
29+
self.board.columns.iter().map(|col| col.tasks.len()).sum()
30+
}
2631
}
2732

2833
impl Task {

src/main.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
mod app;
22
mod board;
33
mod storage;
4+
mod theme;
45
mod ui;
56

67
use app::{App, InputMode};
@@ -71,6 +72,7 @@ fn run_app<B: ratatui::backend::Backend>(
7172
InputMode::ViewingHelp => handle_viewing_help_mode(app, key.code),
7273
InputMode::ProjectList => handle_project_list_mode(app, key.code),
7374
InputMode::AddingProject => handle_adding_project_mode(app, key.code),
75+
InputMode::ConfirmingDelete => handle_confirming_delete_mode(app, key.code),
7476
}
7577
}
7678

@@ -238,7 +240,8 @@ fn handle_project_list_mode(app: &mut App, key: KeyCode) {
238240
KeyCode::Char('k') | KeyCode::Up => app.move_project_up(),
239241
KeyCode::Enter => app.select_project(),
240242
KeyCode::Char('a') => app.start_adding_project(),
241-
KeyCode::Char('d') => app.delete_project(),
243+
KeyCode::Char('d') => app.start_confirming_delete(),
244+
KeyCode::Char('s') => app.set_project_as_default(),
242245
_ => {}
243246
}
244247
}
@@ -256,3 +259,12 @@ fn handle_adding_project_mode(app: &mut App, key: KeyCode) {
256259
_ => {}
257260
}
258261
}
262+
263+
// handle keys when confirming project deletion
264+
fn handle_confirming_delete_mode(app: &mut App, key: KeyCode) {
265+
match key {
266+
KeyCode::Char('y') | KeyCode::Char('Y') => app.confirm_delete_project(),
267+
KeyCode::Esc | KeyCode::Char('n') | KeyCode::Char('N') => app.cancel_delete(),
268+
_ => {}
269+
}
270+
}

src/storage.rs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ use serde::{Deserialize, Serialize};
44
use std::fs;
55
use std::path::PathBuf;
66

7+
// Config struct for storing application settings
8+
#[derive(Deserialize, Serialize, Debug, Clone)]
9+
pub struct Config {
10+
pub default_project: Option<String>,
11+
pub theme: Option<String>,
12+
}
13+
714
// This struct represents the old Board structure for migration purposes
815
#[derive(Deserialize, Serialize, Debug, Clone)]
916
struct LegacyBoard {
@@ -60,7 +67,7 @@ impl From<LegacyProject> for Project {
6067
}
6168
}
6269

63-
// get path to config file
70+
// get path to projects file
6471
fn get_config_path() -> PathBuf {
6572
// ProjectDirs auto find config
6673
if let Some(proj_dirs) = ProjectDirs::from("", "", "tui-kanban") {
@@ -75,6 +82,17 @@ fn get_config_path() -> PathBuf {
7582
}
7683
}
7784

85+
// get path to config.json file
86+
fn get_app_config_path() -> PathBuf {
87+
if let Some(proj_dirs) = ProjectDirs::from("", "", "tui-kanban") {
88+
let config_dir = proj_dirs.config_dir();
89+
fs::create_dir_all(config_dir).ok();
90+
config_dir.join("config.json")
91+
} else {
92+
PathBuf::from("config.json")
93+
}
94+
}
95+
7896
// get old omarchy-kanban config path for migration
7997
fn get_old_omarchy_config_path() -> PathBuf {
8098
if let Some(proj_dirs) = ProjectDirs::from("", "", "omarchy-kanban") {
@@ -150,3 +168,41 @@ pub fn load_projects() -> Vec<Project> {
150168
let default_project = Project::new("Default".to_string());
151169
vec![default_project]
152170
}
171+
172+
/// save config to disc
173+
pub fn save_config(config: &Config) -> Result<(), Box<dyn std::error::Error>> {
174+
let path = get_app_config_path();
175+
let json = serde_json::to_string_pretty(config)?;
176+
fs::write(path, json)?;
177+
Ok(())
178+
}
179+
180+
/// load config from disc
181+
pub fn load_config() -> Config {
182+
let path = get_app_config_path();
183+
184+
if path.exists() {
185+
if let Ok(content) = fs::read_to_string(&path) {
186+
if let Ok(config) = serde_json::from_str::<Config>(&content) {
187+
return config;
188+
}
189+
}
190+
}
191+
192+
// Return default config if file doesn't exist or can't be read
193+
Config {
194+
default_project: None,
195+
theme: Some("high-contrast".to_string()),
196+
}
197+
}
198+
199+
/// check current directory for .tui-kanban-project file
200+
pub fn get_directory_project() -> Option<String> {
201+
if let Ok(content) = fs::read_to_string(".tui-kanban-project") {
202+
let project_name = content.trim().to_string();
203+
if !project_name.is_empty() {
204+
return Some(project_name);
205+
}
206+
}
207+
None
208+
}

0 commit comments

Comments
 (0)