use ratatui::{
layout::{Constraint, Direction, Layout},
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
Frame,
};
use crate::app::{App, InputMode};
use crate::config::Config;
use crate::integrations::IntegrationKind;
fn label_style(is_selected: bool) -> Style {
if is_selected {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Gray)
}
}
pub fn string_to_color(color_str: &str) -> Color {
match color_str {
"Blue" => Color::Blue,
"Cyan" => Color::Cyan,
"Green" => Color::Green,
"Yellow" => Color::Yellow,
"Magenta" => Color::Magenta,
"Red" => Color::Red,
"LightBlue" => Color::LightBlue,
"LightCyan" => Color::LightCyan,
"LightGreen" => Color::LightGreen,
"LightYellow" => Color::LightYellow,
"LightMagenta" => Color::LightMagenta,
"LightRed" => Color::LightRed,
"DarkGray" => Color::DarkGray,
"Gray" => Color::Gray,
"White" => Color::White,
"Black" => Color::Black,
_ => Color::White,
}
}
pub fn draw_settings(f: &mut Frame, app: &App) {
if let InputMode::Settings {
integration,
open_command,
open_worklog_command,
jira_url_setting,
jira_email,
jira_api_token,
date_format,
legacy_time_format,
hide_eye_candy,
colors,
current_field,
cursor_pos,
debug_log_scroll_offset,
..
} = &app.input_mode
{
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), Constraint::Min(10), Constraint::Length(10), Constraint::Length(3), ])
.split(f.area());
let header_text = vec![Line::from(vec![Span::styled(
"⚙ Settings",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)])];
let header = Paragraph::new(header_text).block(Block::default().borders(Borders::ALL));
f.render_widget(header, chunks[0]);
let mut lines = vec![];
let integration_style = if *current_field == 0 {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default()
};
let mut integration_spans = vec![
Span::styled(" Integration: ", label_style(*current_field == 0)),
];
if *current_field == 0 {
integration_spans.push(Span::styled(
"◀ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
));
} else {
integration_spans.push(Span::raw(" "));
}
integration_spans.push(Span::styled(
integration.display_name(),
integration_style,
));
if *current_field == 0 {
integration_spans.push(Span::styled(
" ▶",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
));
}
lines.push(Line::from(integration_spans));
lines.push(Line::from(""));
match integration {
IntegrationKind::CustomCommands => {
draw_custom_commands_fields(
&mut lines,
open_command,
open_worklog_command,
date_format,
*legacy_time_format,
*hide_eye_candy,
colors,
*current_field,
*cursor_pos,
);
}
IntegrationKind::Jira => {
draw_jira_fields(
&mut lines,
jira_url_setting,
jira_email,
jira_api_token,
date_format,
*legacy_time_format,
*hide_eye_candy,
colors,
*current_field,
*cursor_pos,
);
}
}
let settings_form = Paragraph::new(lines).block(
Block::default()
.borders(Borders::ALL)
.title("Configuration"),
);
f.render_widget(settings_form, chunks[1]);
let log_height = chunks[2].height.saturating_sub(2) as usize;
let total_logs = app.debug_log.len();
let scroll_offset = *debug_log_scroll_offset;
let log_lines: Vec<Line> = app.debug_log
.iter()
.rev()
.skip(scroll_offset)
.take(log_height)
.map(|entry| {
let (prefix, rest) = if let Some(idx) = entry.find(']') {
entry.split_at(idx + 1)
} else {
("", entry.as_str())
};
let color = if prefix.contains("ERROR") {
Color::Red
} else if prefix.contains("JIRA") {
Color::Blue
} else if prefix.contains("LOG WORK") {
Color::Cyan
} else if prefix.contains("OPEN ISSUE") {
Color::Magenta
} else if prefix.contains("CLIPBOARD") {
Color::Yellow
} else {
Color::Green
};
Line::from(vec![
Span::styled(prefix, Style::default().fg(color).add_modifier(Modifier::BOLD)),
Span::styled(rest, Style::default().fg(Color::Gray)),
])
})
.collect();
let title = if total_logs > log_height {
format!("Debug Log ({}/{} - Shift+↑/↓ or Ctrl+↑/↓ to scroll)",
scroll_offset + 1.min(total_logs),
total_logs)
} else {
format!("Debug Log ({} entries)", total_logs)
};
let debug_log = Paragraph::new(log_lines).block(
Block::default()
.borders(Borders::ALL)
.title(title),
);
f.render_widget(debug_log, chunks[2]);
let help_text = vec![
Line::from(vec![
Span::styled("↑/↓/Tab", Style::default().fg(Color::Cyan)),
Span::raw(": Navigate "),
Span::styled("←/→", Style::default().fg(Color::Cyan)),
Span::raw(": Change Color "),
Span::styled("Enter", Style::default().fg(Color::Green)),
Span::raw(": Save "),
Span::styled("Esc", Style::default().fg(Color::Red)),
Span::raw(": Cancel"),
]),
];
let footer = Paragraph::new(help_text).block(Block::default().borders(Borders::ALL));
f.render_widget(footer, chunks[3]);
}
}
fn draw_custom_commands_fields(
lines: &mut Vec<Line<'_>>,
open_command: &str,
open_worklog_command: &str,
date_format: &str,
legacy_time_format: bool,
hide_eye_candy: bool,
colors: &[String; 6],
current_field: usize,
cursor_pos: usize,
) {
use crate::cursor::render_with_cursor;
let is_cmd = current_field == 1;
let open_command_style = if is_cmd {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default()
};
lines.push(Line::from(vec![
Span::styled(" Log Work Command (l): ", label_style(is_cmd)),
]));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(render_with_cursor(open_command, cursor_pos, is_cmd), open_command_style),
]));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
"Variables: [[issue_key]] [[entry_started]] [[entry_ended]] [[task_duration]]",
Style::default().fg(Color::DarkGray),
),
]));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
"Press Ctrl++ to insert variable",
Style::default().fg(Color::DarkGray),
),
]));
lines.push(Line::from(""));
let is_wl = current_field == 2;
let open_worklog_command_style = if is_wl {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default()
};
lines.push(Line::from(vec![
Span::styled(" Open Issue Command (o): ", label_style(is_wl)),
]));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(render_with_cursor(open_worklog_command, cursor_pos, is_wl), open_worklog_command_style),
]));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
"Variables: [[issue_key]] [[entry_started]] [[entry_ended]] [[task_duration]]",
Style::default().fg(Color::DarkGray),
),
]));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
"Press Ctrl++ to insert variable",
Style::default().fg(Color::DarkGray),
),
]));
lines.push(Line::from(""));
draw_date_format_field(lines, date_format, current_field == 3, cursor_pos);
lines.push(Line::from(""));
draw_legacy_time_format_field(lines, legacy_time_format, current_field == 4);
lines.push(Line::from(""));
draw_passphrase_button(lines, current_field == 5);
lines.push(Line::from(""));
draw_triggers_button(lines, current_field == 6);
lines.push(Line::from(""));
draw_hide_eye_candy_field(lines, hide_eye_candy, current_field == 7);
lines.push(Line::from(""));
draw_color_fields(lines, colors, 8, current_field);
}
fn draw_jira_fields(
lines: &mut Vec<Line<'_>>,
jira_url_setting: &str,
jira_email: &str,
jira_api_token: &str,
date_format: &str,
legacy_time_format: bool,
hide_eye_candy: bool,
colors: &[String; 6],
current_field: usize,
cursor_pos: usize,
) {
use crate::cursor::render_with_cursor;
let is_url = current_field == 1;
let jira_url_style = if is_url {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default()
};
lines.push(Line::from(vec![
Span::styled(" Jira URL: ", label_style(is_url)),
Span::styled(render_with_cursor(jira_url_setting, cursor_pos, is_url), jira_url_style),
]));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
"e.g., https://yourcompany.atlassian.net",
Style::default().fg(Color::DarkGray),
),
]));
lines.push(Line::from(""));
let is_email = current_field == 2;
let jira_email_style = if is_email {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default()
};
lines.push(Line::from(vec![
Span::styled(" Jira Email: ", label_style(is_email)),
Span::styled(render_with_cursor(jira_email, cursor_pos, is_email), jira_email_style),
]));
lines.push(Line::from(""));
let is_token = current_field == 3;
let token_style = if is_token {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default()
};
let masked_token = if jira_api_token.is_empty() && !is_token {
"(not set)".to_string()
} else if is_token {
let masked = "*".repeat(jira_api_token.chars().count());
render_with_cursor(&masked, cursor_pos, true)
} else {
"*".repeat(jira_api_token.chars().count().min(20))
};
lines.push(Line::from(vec![
Span::styled(" API Token: ", label_style(is_token)),
Span::styled(masked_token, token_style),
]));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
"Generate at: https://id.atlassian.net/manage-profile/security/api-tokens",
Style::default().fg(Color::DarkGray),
),
]));
lines.push(Line::from(""));
draw_date_format_field(lines, date_format, current_field == 4, cursor_pos);
lines.push(Line::from(""));
draw_legacy_time_format_field(lines, legacy_time_format, current_field == 5);
lines.push(Line::from(""));
draw_passphrase_button(lines, current_field == 6);
lines.push(Line::from(""));
draw_triggers_button(lines, current_field == 7);
lines.push(Line::from(""));
draw_hide_eye_candy_field(lines, hide_eye_candy, current_field == 8);
lines.push(Line::from(""));
draw_color_fields(lines, colors, 9, current_field);
}
fn draw_date_format_field(lines: &mut Vec<Line<'_>>, date_format: &str, is_selected: bool, cursor_pos: usize) {
use crate::cursor::render_with_cursor;
let style = if is_selected {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default()
};
lines.push(Line::from(vec![
Span::styled(" Date Format: ", label_style(is_selected)),
Span::styled(render_with_cursor(date_format, cursor_pos, is_selected), style),
Span::styled(" (e.g., %d.%m.-%y)", Style::default().fg(Color::DarkGray)),
]));
}
fn draw_legacy_time_format_field(lines: &mut Vec<Line<'_>>, legacy_time_format: bool, is_selected: bool) {
let style = if is_selected {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default()
};
let value = if legacy_time_format { "Yes" } else { "No" };
lines.push(Line::from(vec![
Span::styled(" Legacy Time Format: ", label_style(is_selected)),
Span::styled(value, style),
Span::styled(" (←/→ or Space to toggle)", Style::default().fg(Color::DarkGray)),
]));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
"Format: 2025-11-06T14:25:00.000+0000 (no colon in timezone)",
Style::default().fg(Color::DarkGray),
),
]));
}
fn draw_hide_eye_candy_field(lines: &mut Vec<Line<'_>>, hide_eye_candy: bool, is_selected: bool) {
let style = if is_selected {
Style::default().bg(Color::DarkGray).fg(Color::White)
} else {
Style::default()
};
let value = if hide_eye_candy { "Yes" } else { "No" };
lines.push(Line::from(vec![
Span::styled(" Hide Eye Candy: ", label_style(is_selected)),
Span::styled(value, style),
Span::styled(" (←/→ or Space to toggle)", Style::default().fg(Color::DarkGray)),
]));
lines.push(Line::from(vec![
Span::raw(" "),
Span::styled(
"Turn off the shimmer and breathe animations on the main view.",
Style::default().fg(Color::DarkGray),
),
]));
}
fn draw_passphrase_button(lines: &mut Vec<Line<'_>>, is_selected: bool) {
let style = if is_selected {
Style::default()
.bg(Color::DarkGray)
.fg(Color::White)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
lines.push(Line::from(vec![
Span::styled(" Passphrase: ", label_style(is_selected)),
Span::styled("Change Passphrase [Enter]", style),
]));
}
fn draw_triggers_button(lines: &mut Vec<Line<'_>>, is_selected: bool) {
let style = if is_selected {
Style::default()
.bg(Color::DarkGray)
.fg(Color::White)
.add_modifier(Modifier::BOLD)
} else {
Style::default()
};
lines.push(Line::from(vec![
Span::styled(" Triggers: ", label_style(is_selected)),
Span::styled("Configure Event Webhooks [Enter]", style),
]));
}
fn draw_color_fields(
lines: &mut Vec<Line<'_>>,
colors: &[String; 6],
colors_start: usize,
current_field: usize,
) {
lines.push(Line::from(Span::styled(
" Color Palette:",
Style::default()
.fg(Color::Cyan)
.add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(""));
for (idx, color_name) in Config::color_names().iter().enumerate() {
let field_idx = colors_start + idx;
let is_selected = current_field == field_idx;
let color_value = &colors[idx];
let actual_color = string_to_color(color_value);
let name_style = if is_selected {
Style::default()
.fg(Color::Yellow)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(Color::Gray)
};
let mut spans = vec![
Span::raw(" "),
Span::styled(format!("{:<10}", color_name), name_style),
Span::raw(" "),
];
if is_selected {
spans.push(Span::styled(
"◀ ",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
));
} else {
spans.push(Span::raw(" "));
}
spans.push(Span::styled(
"███",
Style::default()
.fg(actual_color)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::raw(" "));
let value_style = if is_selected {
Style::default()
.bg(Color::DarkGray)
.fg(Color::White)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(actual_color)
};
spans.push(Span::styled(format!("{:<15}", color_value), value_style));
if is_selected {
spans.push(Span::styled(
" ▶",
Style::default()
.fg(Color::White)
.add_modifier(Modifier::BOLD),
));
}
lines.push(Line::from(spans));
}
}