datepick 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +182 -0
- data/bin/datepick +10 -0
- data/lib/datepick.rb +501 -0
- metadata +64 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 42e1650fbe65ad40a8bab0a5629d85aa2b43a6b5be8087058058d1f826a05a03
|
4
|
+
data.tar.gz: eb804269ae096c685567eb409c5c40f158bf0ca4b71ee0c1567f02a1f8caf8af
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 924025949e01cb91dd1542542334007883646c5bbb51c6a373b28ff9f58df24eb49b19c5b5534d52fdf39145d64d73937fec5e2bf1b5843c101636bbf088baba
|
7
|
+
data.tar.gz: 1470fe30803a6860d468c86f1afbbbe1557ebb3a674f7b9ba7c08f964df7320c0e1cc67af57e2f16f271f9e3c741a81bb495970180870c25a8467a8bf5566f1b
|
data/README.md
ADDED
@@ -0,0 +1,182 @@
|
|
1
|
+
# Datepick - Interactive Terminal Date Picker
|
2
|
+
|
3
|
+
A powerful, interactive terminal date picker built with Ruby and rcurses. Features vim-style navigation, configurable date formats, multiple month views, and extensive keyboard shortcuts. Perfect for shell scripts and command-line workflows that need date selection.
|
4
|
+
|
5
|
+
## Features
|
6
|
+
|
7
|
+
- **Interactive calendar display** with multiple months
|
8
|
+
- **Vim-style navigation** with hjkl keys and numeric prefixes
|
9
|
+
- **Configurable date output formats** with quick presets
|
10
|
+
- **Customizable multi-month view** (months before/after current)
|
11
|
+
- **Week start preference** (Monday or Sunday)
|
12
|
+
- **Visual highlights** for today, selected date, and weekends
|
13
|
+
- **Flicker-free rendering** using rcurses
|
14
|
+
- **Persistent configuration** saved to `~/.datepick`
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
Install from RubyGems:
|
19
|
+
|
20
|
+
```bash
|
21
|
+
gem install datepick
|
22
|
+
```
|
23
|
+
|
24
|
+
## Usage
|
25
|
+
|
26
|
+
Simply run `datepick` in your terminal:
|
27
|
+
|
28
|
+
```bash
|
29
|
+
datepick
|
30
|
+
```
|
31
|
+
|
32
|
+
The selected date will be printed to stdout when you press Enter, making it perfect for shell scripts:
|
33
|
+
|
34
|
+
```bash
|
35
|
+
# Capture selected date in a variable
|
36
|
+
selected_date=$(datepick)
|
37
|
+
echo "You selected: $selected_date"
|
38
|
+
|
39
|
+
# Use in file operations
|
40
|
+
cp important_file.txt "backup_$(datepick).txt"
|
41
|
+
```
|
42
|
+
|
43
|
+
## Keyboard Navigation
|
44
|
+
|
45
|
+
### Basic Movement
|
46
|
+
- **Arrow keys** or **hjkl** - Navigate between dates
|
47
|
+
- **Enter** - Select current date and exit
|
48
|
+
- **q** - Quit without selecting
|
49
|
+
|
50
|
+
### Advanced Navigation
|
51
|
+
- **n/p** - Next/Previous month
|
52
|
+
- **N/P** - Next/Previous year
|
53
|
+
- **t** - Jump to today
|
54
|
+
- **H/^** - Go to start of week
|
55
|
+
- **L/$** - Go to end of week
|
56
|
+
- **Home** - Go to start of month
|
57
|
+
- **End** - Go to end of month
|
58
|
+
|
59
|
+
### Vim-style Jumps
|
60
|
+
- **[number]g** - Jump forward by number of days
|
61
|
+
- Example: `7g` jumps 7 days ahead
|
62
|
+
- Example: `30g` jumps 30 days ahead
|
63
|
+
|
64
|
+
### Configuration
|
65
|
+
- **c** - Enter configuration mode
|
66
|
+
- **r** - Force refresh display
|
67
|
+
|
68
|
+
## Date Format Options
|
69
|
+
|
70
|
+
When configuring date format, you can either:
|
71
|
+
|
72
|
+
1. **Use quick presets** by entering a number (1-8):
|
73
|
+
- `1`: 2025-07-01 (ISO format)
|
74
|
+
- `2`: 01/07/2025 (European)
|
75
|
+
- `3`: 07/01/2025 (US format)
|
76
|
+
- `4`: July 01, 2025 (Long format)
|
77
|
+
- `5`: Jul 01, 2025 (Abbreviated)
|
78
|
+
- `6`: 20250701 (Compact)
|
79
|
+
- `7`: 01-Jul-2025 (DD-Mon-YYYY)
|
80
|
+
- `8`: Tuesday, July 01, 2025 (Full with weekday)
|
81
|
+
|
82
|
+
2. **Enter custom format** using Ruby's strftime syntax:
|
83
|
+
- `%Y-%m-%d` - ISO format (default)
|
84
|
+
- `%d/%m/%Y` - European format
|
85
|
+
- `%B %d, %Y` - Long format
|
86
|
+
- See [Ruby strftime documentation](https://ruby-doc.org/core/Time.html#method-i-strftime) for all options
|
87
|
+
|
88
|
+
## Configuration Options
|
89
|
+
|
90
|
+
Access configuration by pressing `c`:
|
91
|
+
|
92
|
+
- **Date format**: Output format for selected date
|
93
|
+
- **Months before**: Number of months to show before current month
|
94
|
+
- **Months after**: Number of months to show after current month
|
95
|
+
- **Week starts Monday**: Toggle between Monday/Sunday week start
|
96
|
+
|
97
|
+
Configuration is automatically saved to `~/.datepick` and persists between sessions.
|
98
|
+
|
99
|
+
## Visual Features
|
100
|
+
|
101
|
+
- **Current month**: Bold and underlined month header
|
102
|
+
- **Today's date**: Highlighted in magenta and bold
|
103
|
+
- **Selected date**: Yellow background with bold text
|
104
|
+
- **Weekends**: Red text for Saturday/Sunday
|
105
|
+
- **Day headers**: Bold with darker colors for better visibility
|
106
|
+
|
107
|
+
## Shell Integration Examples
|
108
|
+
|
109
|
+
### Bash Scripts
|
110
|
+
```bash
|
111
|
+
#!/bin/bash
|
112
|
+
echo "Select a date for the backup:"
|
113
|
+
backup_date=$(datepick)
|
114
|
+
tar -czf "backup_${backup_date}.tar.gz" /important/files/
|
115
|
+
```
|
116
|
+
|
117
|
+
### ZSH Function
|
118
|
+
```zsh
|
119
|
+
# Add to your .zshrc
|
120
|
+
function schedule() {
|
121
|
+
local date=$(datepick)
|
122
|
+
echo "Scheduled for: $date"
|
123
|
+
# Add your scheduling logic here
|
124
|
+
}
|
125
|
+
```
|
126
|
+
|
127
|
+
### Git Commits with Specific Dates
|
128
|
+
```bash
|
129
|
+
# Select a date and create a commit with that date
|
130
|
+
commit_date=$(datepick)
|
131
|
+
git commit --date="$commit_date" -m "Your commit message"
|
132
|
+
```
|
133
|
+
|
134
|
+
## Requirements
|
135
|
+
|
136
|
+
- Ruby 2.7 or higher
|
137
|
+
- rcurses gem (automatically installed)
|
138
|
+
- Terminal with color support
|
139
|
+
|
140
|
+
## Development
|
141
|
+
|
142
|
+
Clone the repository:
|
143
|
+
```bash
|
144
|
+
git clone https://github.com/isene/datepick.git
|
145
|
+
cd datepick
|
146
|
+
```
|
147
|
+
|
148
|
+
Install dependencies:
|
149
|
+
```bash
|
150
|
+
bundle install
|
151
|
+
```
|
152
|
+
|
153
|
+
Run locally:
|
154
|
+
```bash
|
155
|
+
ruby -Ilib bin/datepick
|
156
|
+
```
|
157
|
+
|
158
|
+
## Contributing
|
159
|
+
|
160
|
+
1. Fork the repository
|
161
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
162
|
+
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
163
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
164
|
+
5. Open a Pull Request
|
165
|
+
|
166
|
+
## Inspiration
|
167
|
+
|
168
|
+
Inspired by [pickdate](https://github.com/maraloon/pickdate) and designed to integrate seamlessly with terminal workflows.
|
169
|
+
|
170
|
+
## License
|
171
|
+
|
172
|
+
This project is released under the Unlicense - see the project repository for details.
|
173
|
+
|
174
|
+
## Author
|
175
|
+
|
176
|
+
Created by [Geir Isene](https://isene.com/)
|
177
|
+
|
178
|
+
## Links
|
179
|
+
|
180
|
+
- **Homepage**: https://isene.com/
|
181
|
+
- **Source Code**: https://github.com/isene/datepick
|
182
|
+
- **RubyGems**: https://rubygems.org/gems/datepick
|
data/bin/datepick
ADDED
data/lib/datepick.rb
ADDED
@@ -0,0 +1,501 @@
|
|
1
|
+
require 'date'
|
2
|
+
require 'json'
|
3
|
+
require 'rcurses'
|
4
|
+
|
5
|
+
class DatePicker
|
6
|
+
include Rcurses::Input
|
7
|
+
|
8
|
+
CONFIG_FILE = File.expand_path('~/.datepick')
|
9
|
+
|
10
|
+
DEFAULT_CONFIG = {
|
11
|
+
'date_format' => '%Y-%m-%d',
|
12
|
+
'months_before' => 1,
|
13
|
+
'months_after' => 1,
|
14
|
+
'week_starts_monday' => true,
|
15
|
+
'highlight_weekends' => true,
|
16
|
+
'colors' => {
|
17
|
+
'year' => 14, # cyan
|
18
|
+
'month' => 10, # green
|
19
|
+
'day' => 15, # white
|
20
|
+
'selected' => 11, # yellow
|
21
|
+
'today' => 13, # magenta
|
22
|
+
'weekend' => 9 # red
|
23
|
+
}
|
24
|
+
}.freeze
|
25
|
+
|
26
|
+
# Common date formats for quick selection
|
27
|
+
DATE_FORMATS = {
|
28
|
+
'1' => '%Y-%m-%d', # ISO format
|
29
|
+
'2' => '%d/%m/%Y', # European
|
30
|
+
'3' => '%m/%d/%Y', # US format
|
31
|
+
'4' => '%B %d, %Y', # Long format
|
32
|
+
'5' => '%b %d, %Y', # Abbreviated
|
33
|
+
'6' => '%Y%m%d', # Compact
|
34
|
+
'7' => '%d-%b-%Y', # DD-Mon-YYYY
|
35
|
+
'8' => '%A, %B %d, %Y' # Full with weekday
|
36
|
+
}
|
37
|
+
|
38
|
+
def initialize
|
39
|
+
@config = load_config
|
40
|
+
@selected_date = Date.today
|
41
|
+
@current_month = Date.today
|
42
|
+
@config_mode = false
|
43
|
+
@config_selected = 0
|
44
|
+
@screen_w = `tput cols`.to_i
|
45
|
+
@screen_h = `tput lines`.to_i
|
46
|
+
|
47
|
+
# Initialize panes
|
48
|
+
@main_pane = Rcurses::Pane.new(1, 1, @screen_w, @screen_h - 4, nil, nil)
|
49
|
+
@main_pane.border = false
|
50
|
+
|
51
|
+
@help_pane = Rcurses::Pane.new(1, @screen_h - 3, @screen_w, 1, nil, nil)
|
52
|
+
@help_pane.border = false
|
53
|
+
|
54
|
+
@status_pane = Rcurses::Pane.new(1, @screen_h - 1, @screen_w, 1, nil, nil)
|
55
|
+
@status_pane.border = false
|
56
|
+
|
57
|
+
@prev_content = ""
|
58
|
+
end
|
59
|
+
|
60
|
+
def run
|
61
|
+
Rcurses.init!
|
62
|
+
Rcurses::Cursor.hide
|
63
|
+
|
64
|
+
begin
|
65
|
+
# Initial display
|
66
|
+
render
|
67
|
+
|
68
|
+
loop do
|
69
|
+
input = handle_input
|
70
|
+
break if input == :exit
|
71
|
+
render
|
72
|
+
end
|
73
|
+
rescue Interrupt
|
74
|
+
# Exit cleanly on Ctrl-C
|
75
|
+
ensure
|
76
|
+
Rcurses.cleanup!
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def load_config
|
83
|
+
if File.exist?(CONFIG_FILE)
|
84
|
+
JSON.parse(File.read(CONFIG_FILE))
|
85
|
+
else
|
86
|
+
DEFAULT_CONFIG.dup
|
87
|
+
end
|
88
|
+
rescue JSON::ParserError
|
89
|
+
DEFAULT_CONFIG.dup
|
90
|
+
end
|
91
|
+
|
92
|
+
def save_config
|
93
|
+
File.write(CONFIG_FILE, JSON.pretty_generate(@config))
|
94
|
+
end
|
95
|
+
|
96
|
+
def render
|
97
|
+
if @config_mode
|
98
|
+
render_config
|
99
|
+
else
|
100
|
+
render_calendar
|
101
|
+
end
|
102
|
+
|
103
|
+
# Update help text
|
104
|
+
help_text = if @config_mode
|
105
|
+
"Navigate: ↑↓ | Edit: Enter | Cancel: Esc".fg(245)
|
106
|
+
else
|
107
|
+
help_parts = []
|
108
|
+
help_parts << "←↓↑→/hjkl" if @numeric_prefix.nil? || @numeric_prefix.empty?
|
109
|
+
help_parts << "#{@numeric_prefix}g:jump #{@numeric_prefix} days" if @numeric_prefix && !@numeric_prefix.empty?
|
110
|
+
help_parts << "n/p:month | N/P:year | t:today | H/L:week | Home/End:month | Enter:select | c:config | q:quit"
|
111
|
+
help_parts.join(" | ").fg(245)
|
112
|
+
end
|
113
|
+
|
114
|
+
@help_pane.text = help_text
|
115
|
+
@help_pane.refresh
|
116
|
+
|
117
|
+
# Update status
|
118
|
+
status_text = "Selected: #{@selected_date.strftime(@config['date_format'])}".fg(@config['colors']['selected'])
|
119
|
+
@status_pane.text = status_text
|
120
|
+
@status_pane.refresh
|
121
|
+
end
|
122
|
+
|
123
|
+
def render_calendar
|
124
|
+
content = generate_calendar_content
|
125
|
+
|
126
|
+
# Only update if content changed
|
127
|
+
if content != @prev_content
|
128
|
+
@main_pane.text = content
|
129
|
+
@main_pane.refresh
|
130
|
+
@prev_content = content
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
def generate_calendar_content
|
135
|
+
lines = []
|
136
|
+
months_to_display = []
|
137
|
+
|
138
|
+
# Calculate range of months to display
|
139
|
+
start_month = @current_month.prev_month(@config['months_before'])
|
140
|
+
end_month = @current_month.next_month(@config['months_after'])
|
141
|
+
|
142
|
+
current = start_month
|
143
|
+
while current <= end_month
|
144
|
+
months_to_display << current
|
145
|
+
current = current.next_month
|
146
|
+
end
|
147
|
+
|
148
|
+
# Generate months horizontally
|
149
|
+
months_per_row = [(@screen_w - 4) / 22, 1].max
|
150
|
+
|
151
|
+
months_to_display.each_slice(months_per_row) do |month_group|
|
152
|
+
# Generate this row of months
|
153
|
+
month_lines = generate_month_row(month_group)
|
154
|
+
lines.concat(month_lines)
|
155
|
+
lines << "" # Add spacing between month rows
|
156
|
+
end
|
157
|
+
|
158
|
+
lines.join("\n")
|
159
|
+
end
|
160
|
+
|
161
|
+
def generate_month_row(months)
|
162
|
+
result_lines = []
|
163
|
+
max_weeks = 6
|
164
|
+
|
165
|
+
# Header line with month names
|
166
|
+
header_line = ""
|
167
|
+
months.each_with_index do |month_date, idx|
|
168
|
+
month_str = month_date.strftime("%B %Y")
|
169
|
+
# Highlight current month with bold and underline
|
170
|
+
if month_date.year == Date.today.year && month_date.month == Date.today.month
|
171
|
+
month_str = month_str.fg(@config['colors']['month']).b.u
|
172
|
+
else
|
173
|
+
month_str = month_str.fg(@config['colors']['month'])
|
174
|
+
end
|
175
|
+
header_line += month_str.ljust(22 + month_str.length - month_date.strftime("%B %Y").length)
|
176
|
+
end
|
177
|
+
result_lines << header_line
|
178
|
+
|
179
|
+
# Day headers
|
180
|
+
day_header_line = ""
|
181
|
+
months.each do |month_date|
|
182
|
+
days = @config['week_starts_monday'] ? %w[Mo Tu We Th Fr Sa Su] : %w[Su Mo Tu We Th Fr Sa]
|
183
|
+
days.each_with_index do |day, idx|
|
184
|
+
# Use darker colors for day headers and make them bold
|
185
|
+
color = ((@config['week_starts_monday'] && idx >= 5) || (!@config['week_starts_monday'] && (idx == 0 || idx == 6))) ?
|
186
|
+
88 : 244 # Dark red for weekends, dark gray for weekdays
|
187
|
+
day_header_line += day.fg(color).b + " "
|
188
|
+
end
|
189
|
+
day_header_line += " " # Extra space between months
|
190
|
+
end
|
191
|
+
result_lines << day_header_line
|
192
|
+
|
193
|
+
# Generate week lines
|
194
|
+
week_data = months.map { |m| generate_month_weeks(m) }
|
195
|
+
|
196
|
+
(0...max_weeks).each do |week_idx|
|
197
|
+
week_line = ""
|
198
|
+
months.each_with_index do |month_date, month_idx|
|
199
|
+
week = week_data[month_idx][week_idx] || []
|
200
|
+
|
201
|
+
7.times do |day_idx|
|
202
|
+
if week[day_idx]
|
203
|
+
date = week[day_idx]
|
204
|
+
day_str = date.day.to_s.rjust(2)
|
205
|
+
|
206
|
+
# Apply styling
|
207
|
+
if date == @selected_date
|
208
|
+
day_str = day_str.fb(@config['colors']['selected'], 236).b
|
209
|
+
elsif date == Date.today
|
210
|
+
day_str = day_str.fg(@config['colors']['today']).b
|
211
|
+
elsif date.saturday? || date.sunday?
|
212
|
+
day_str = day_str.fg(@config['colors']['weekend'])
|
213
|
+
else
|
214
|
+
day_str = day_str.fg(@config['colors']['day'])
|
215
|
+
end
|
216
|
+
|
217
|
+
week_line += day_str + " "
|
218
|
+
else
|
219
|
+
week_line += " "
|
220
|
+
end
|
221
|
+
end
|
222
|
+
week_line += " " # Extra space between months
|
223
|
+
end
|
224
|
+
result_lines << week_line unless week_line.strip.empty?
|
225
|
+
end
|
226
|
+
|
227
|
+
result_lines
|
228
|
+
end
|
229
|
+
|
230
|
+
def generate_month_weeks(month_date)
|
231
|
+
weeks = []
|
232
|
+
current_week = []
|
233
|
+
|
234
|
+
first_day = Date.new(month_date.year, month_date.month, 1)
|
235
|
+
last_day = Date.new(month_date.year, month_date.month, -1)
|
236
|
+
|
237
|
+
# Calculate starting position
|
238
|
+
wday = first_day.wday
|
239
|
+
if @config['week_starts_monday']
|
240
|
+
wday = (wday - 1) % 7
|
241
|
+
end
|
242
|
+
|
243
|
+
# Add empty days at the beginning
|
244
|
+
wday.times { current_week << nil }
|
245
|
+
|
246
|
+
# Add all days of the month
|
247
|
+
(first_day..last_day).each do |date|
|
248
|
+
current_week << date
|
249
|
+
|
250
|
+
# Check if week is complete
|
251
|
+
if @config['week_starts_monday']
|
252
|
+
if date.wday == 0 # Sunday
|
253
|
+
weeks << current_week
|
254
|
+
current_week = []
|
255
|
+
end
|
256
|
+
else
|
257
|
+
if date.wday == 6 # Saturday
|
258
|
+
weeks << current_week
|
259
|
+
current_week = []
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
# Add the last week if it has any days
|
265
|
+
weeks << current_week unless current_week.empty?
|
266
|
+
|
267
|
+
weeks
|
268
|
+
end
|
269
|
+
|
270
|
+
def render_config
|
271
|
+
config_items = [
|
272
|
+
["Date format", @config['date_format']],
|
273
|
+
["Months before", @config['months_before'].to_s],
|
274
|
+
["Months after", @config['months_after'].to_s],
|
275
|
+
["Week starts Monday", @config['week_starts_monday'] ? "Yes" : "No"],
|
276
|
+
["Save and exit config", "Press Enter"]
|
277
|
+
]
|
278
|
+
|
279
|
+
lines = []
|
280
|
+
lines << ""
|
281
|
+
lines << "Configuration".fg(@config['colors']['year']).b
|
282
|
+
lines << ""
|
283
|
+
|
284
|
+
config_items.each_with_index do |(label, value), idx|
|
285
|
+
line = " #{label}: #{value}"
|
286
|
+
if idx == @config_selected
|
287
|
+
line = line.fb(0, 15)
|
288
|
+
end
|
289
|
+
lines << line
|
290
|
+
lines << "" # Add spacing
|
291
|
+
end
|
292
|
+
|
293
|
+
content = lines.join("\n")
|
294
|
+
|
295
|
+
# Always refresh config screen to show updated values
|
296
|
+
@main_pane.text = content
|
297
|
+
@main_pane.refresh
|
298
|
+
@prev_content = content
|
299
|
+
end
|
300
|
+
|
301
|
+
def handle_input
|
302
|
+
ch = getchr
|
303
|
+
|
304
|
+
if @config_mode
|
305
|
+
handle_config_input(ch)
|
306
|
+
else
|
307
|
+
handle_calendar_input(ch)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
def handle_calendar_input(ch)
|
312
|
+
# Reset numeric prefix for non-numeric keys (except 'g')
|
313
|
+
if ch !~ /[0-9g]/ && @numeric_prefix
|
314
|
+
@numeric_prefix = ""
|
315
|
+
end
|
316
|
+
|
317
|
+
case ch
|
318
|
+
when 'q', 'Q'
|
319
|
+
return :exit
|
320
|
+
when 'c', 'C'
|
321
|
+
@config_mode = true
|
322
|
+
@config_selected = 0
|
323
|
+
@prev_content = "" # Force refresh
|
324
|
+
when 'ENTER'
|
325
|
+
Rcurses.cleanup!
|
326
|
+
puts @selected_date.strftime(@config['date_format'])
|
327
|
+
exit
|
328
|
+
when 'LEFT', 'h', 'H'
|
329
|
+
@selected_date = @selected_date.prev_day
|
330
|
+
update_current_month
|
331
|
+
when 'RIGHT', 'l', 'L'
|
332
|
+
@selected_date = @selected_date.next_day
|
333
|
+
update_current_month
|
334
|
+
when 'UP', 'k', 'K'
|
335
|
+
@selected_date = @selected_date.prev_day(7)
|
336
|
+
update_current_month
|
337
|
+
when 'DOWN', 'j', 'J'
|
338
|
+
@selected_date = @selected_date.next_day(7)
|
339
|
+
update_current_month
|
340
|
+
when 'w', 'W'
|
341
|
+
@selected_date = @selected_date.next_day(7)
|
342
|
+
update_current_month
|
343
|
+
when 'b', 'B'
|
344
|
+
@selected_date = @selected_date.prev_day(7)
|
345
|
+
update_current_month
|
346
|
+
when 'n'
|
347
|
+
@selected_date = @selected_date.next_month
|
348
|
+
update_current_month
|
349
|
+
when 'p'
|
350
|
+
@selected_date = @selected_date.prev_month
|
351
|
+
update_current_month
|
352
|
+
when 'N'
|
353
|
+
@selected_date = @selected_date.next_year
|
354
|
+
update_current_month
|
355
|
+
when 'P'
|
356
|
+
@selected_date = @selected_date.prev_year
|
357
|
+
update_current_month
|
358
|
+
when 'H', '^'
|
359
|
+
# Go to start of week
|
360
|
+
days_back = @config['week_starts_monday'] ?
|
361
|
+
(@selected_date.wday == 0 ? 6 : @selected_date.wday - 1) :
|
362
|
+
@selected_date.wday
|
363
|
+
@selected_date = @selected_date.prev_day(days_back)
|
364
|
+
update_current_month
|
365
|
+
when 'L', '$'
|
366
|
+
# Go to end of week
|
367
|
+
days_forward = @config['week_starts_monday'] ?
|
368
|
+
(7 - (@selected_date.wday == 0 ? 7 : @selected_date.wday)) :
|
369
|
+
(6 - @selected_date.wday)
|
370
|
+
@selected_date = @selected_date.next_day(days_forward)
|
371
|
+
update_current_month
|
372
|
+
when 'HOME'
|
373
|
+
# Go to start of month
|
374
|
+
@selected_date = Date.new(@selected_date.year, @selected_date.month, 1)
|
375
|
+
update_current_month
|
376
|
+
when 'END'
|
377
|
+
# Go to end of month
|
378
|
+
@selected_date = Date.new(@selected_date.year, @selected_date.month, -1)
|
379
|
+
update_current_month
|
380
|
+
when 't', 'T'
|
381
|
+
# Go to today
|
382
|
+
@selected_date = Date.today
|
383
|
+
@current_month = Date.today
|
384
|
+
when '0'..'9'
|
385
|
+
# Numeric prefix for jumps (vim-style)
|
386
|
+
@numeric_prefix ||= ""
|
387
|
+
@numeric_prefix += ch
|
388
|
+
when 'g'
|
389
|
+
# Execute numeric jump
|
390
|
+
if @numeric_prefix && !@numeric_prefix.empty?
|
391
|
+
days = @numeric_prefix.to_i
|
392
|
+
@selected_date = @selected_date.next_day(days)
|
393
|
+
update_current_month
|
394
|
+
@numeric_prefix = ""
|
395
|
+
end
|
396
|
+
when 'r', 'R'
|
397
|
+
# Force full refresh
|
398
|
+
Rcurses.clear_screen
|
399
|
+
@prev_content = ""
|
400
|
+
@main_pane.full_refresh
|
401
|
+
@help_pane.full_refresh
|
402
|
+
@status_pane.full_refresh
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def handle_config_input(ch)
|
407
|
+
case ch
|
408
|
+
when 'ESC', 'q', 'Q'
|
409
|
+
@config_mode = false
|
410
|
+
@prev_content = "" # Force refresh
|
411
|
+
when 'UP', 'k', 'K'
|
412
|
+
@config_selected = (@config_selected - 1) % 5
|
413
|
+
when 'DOWN', 'j', 'J'
|
414
|
+
@config_selected = (@config_selected + 1) % 5
|
415
|
+
when 'ENTER'
|
416
|
+
handle_config_edit
|
417
|
+
render_config # Immediately re-render config after edit
|
418
|
+
end
|
419
|
+
end
|
420
|
+
|
421
|
+
def handle_config_edit
|
422
|
+
case @config_selected
|
423
|
+
when 0 # Date format
|
424
|
+
format_help = DATE_FORMATS.map { |k, v| "#{k}: #{Date.today.strftime(v)}" }.join(" | ")
|
425
|
+
new_format = get_input_with_help("Date format", @config['date_format'], format_help)
|
426
|
+
# Check if user entered a number for quick format selection
|
427
|
+
if new_format && DATE_FORMATS[new_format]
|
428
|
+
@config['date_format'] = DATE_FORMATS[new_format]
|
429
|
+
elsif new_format && !new_format.empty?
|
430
|
+
@config['date_format'] = new_format
|
431
|
+
end
|
432
|
+
@prev_content = "" # Force refresh to show new value
|
433
|
+
when 1 # Months before
|
434
|
+
old_value = @config['months_before']
|
435
|
+
new_value = get_input("Months before", old_value.to_s)
|
436
|
+
if new_value && new_value != old_value.to_s
|
437
|
+
@config['months_before'] = new_value.to_i
|
438
|
+
end
|
439
|
+
@prev_content = "" # Force refresh to show new value
|
440
|
+
when 2 # Months after
|
441
|
+
new_value = get_input("Months after", @config['months_after'].to_s)
|
442
|
+
if new_value && new_value.to_i > 0
|
443
|
+
@config['months_after'] = new_value.to_i
|
444
|
+
end
|
445
|
+
@prev_content = "" # Force refresh to show new value
|
446
|
+
when 3 # Week starts Monday
|
447
|
+
@config['week_starts_monday'] = !@config['week_starts_monday']
|
448
|
+
@prev_content = "" # Force refresh
|
449
|
+
when 4 # Save and exit
|
450
|
+
save_config
|
451
|
+
@config_mode = false
|
452
|
+
@prev_content = "" # Force refresh
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
def get_input(prompt, default)
|
457
|
+
# Create input pane
|
458
|
+
pane = Rcurses::Pane.new(2, @screen_h - 4, @screen_w - 4, 3, 15, 0)
|
459
|
+
pane.border = true
|
460
|
+
|
461
|
+
# Use ask method which properly handles input
|
462
|
+
result = pane.ask("#{prompt}: ", default)
|
463
|
+
|
464
|
+
# Force a full screen refresh after input dialog
|
465
|
+
Rcurses.clear_screen
|
466
|
+
@main_pane.full_refresh
|
467
|
+
@help_pane.full_refresh
|
468
|
+
@status_pane.full_refresh
|
469
|
+
@prev_content = "" # Force refresh on next render
|
470
|
+
|
471
|
+
# Return the result (ask already returns the text)
|
472
|
+
result.strip
|
473
|
+
end
|
474
|
+
|
475
|
+
def get_input_with_help(prompt, default, help_text)
|
476
|
+
# Create input pane with extra height for help
|
477
|
+
pane = Rcurses::Pane.new(2, @screen_h - 6, @screen_w - 4, 5, 15, 0)
|
478
|
+
pane.border = true
|
479
|
+
|
480
|
+
# Show help text above input
|
481
|
+
Rcurses::Cursor.set(@screen_h - 6, 4)
|
482
|
+
print help_text.fg(245)
|
483
|
+
|
484
|
+
# Use ask method which properly handles input
|
485
|
+
result = pane.ask("#{prompt}: ", default)
|
486
|
+
|
487
|
+
# Force a full screen refresh after input dialog
|
488
|
+
Rcurses.clear_screen
|
489
|
+
@main_pane.full_refresh
|
490
|
+
@help_pane.full_refresh
|
491
|
+
@status_pane.full_refresh
|
492
|
+
@prev_content = "" # Force refresh on next render
|
493
|
+
|
494
|
+
# Return the result (ask already returns the text)
|
495
|
+
result.strip
|
496
|
+
end
|
497
|
+
|
498
|
+
def update_current_month
|
499
|
+
@current_month = Date.new(@selected_date.year, @selected_date.month, 1)
|
500
|
+
end
|
501
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: datepick
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Geir Isene
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2025-07-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rcurses
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '4.8'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '4.8'
|
27
|
+
description: A powerful interactive terminal date picker built with rcurses. Features
|
28
|
+
vim-style navigation, configurable date formats, multiple month views, and extensive
|
29
|
+
keyboard shortcuts. Perfect for shell scripts and command-line workflows that need
|
30
|
+
date selection.
|
31
|
+
email: g@isene.com
|
32
|
+
executables:
|
33
|
+
- datepick
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- README.md
|
38
|
+
- bin/datepick
|
39
|
+
- lib/datepick.rb
|
40
|
+
homepage: https://isene.com/
|
41
|
+
licenses:
|
42
|
+
- Unlicense
|
43
|
+
metadata:
|
44
|
+
source_code_uri: https://github.com/isene/datepick
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
require_paths:
|
48
|
+
- lib
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
requirements:
|
51
|
+
- - ">="
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
55
|
+
requirements:
|
56
|
+
- - ">="
|
57
|
+
- !ruby/object:Gem::Version
|
58
|
+
version: '0'
|
59
|
+
requirements: []
|
60
|
+
rubygems_version: 3.4.20
|
61
|
+
signing_key:
|
62
|
+
specification_version: 4
|
63
|
+
summary: Datepick - Interactive Terminal Date Picker
|
64
|
+
test_files: []
|