terminal_calendar 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- checksums.yaml.gz.sig +0 -0
- data/.rspec +3 -0
- data/.ruby-version +1 -0
- data/LICENSE +7 -0
- data/README.md +63 -0
- data/Rakefile +12 -0
- data/_doc/cal-screenshot.png +0 -0
- data/_doc/date-picker.gif +0 -0
- data/certs/mcordell.pem +26 -0
- data/lib/date_extensions.rb +39 -0
- data/lib/terminal_calendar/date_picker.rb +160 -0
- data/lib/terminal_calendar/month/calendar_day.rb +69 -0
- data/lib/terminal_calendar/month.rb +98 -0
- data/lib/terminal_calendar/selection/cell.rb +74 -0
- data/lib/terminal_calendar/selection/grid.rb +210 -0
- data/lib/terminal_calendar/selection/month_page.rb +68 -0
- data/lib/terminal_calendar/selection/month_year_dialog.rb +99 -0
- data/lib/terminal_calendar/selection/null_cell.rb +32 -0
- data/lib/terminal_calendar/selection/selector.rb +167 -0
- data/lib/terminal_calendar/version.rb +5 -0
- data/lib/terminal_calendar.rb +40 -0
- data/lib/tty/prompt/carousel.rb +115 -0
- data/sig/terminal_calendar.rbs +4 -0
- data/terminal_calendar.gemspec +42 -0
- data.tar.gz.sig +0 -0
- metadata +176 -0
- metadata.gz.sig +2 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 99295fbd7c1284f599d3c5d27f8dab36af0fc2e008043ec74c5bbeb863db29c4
|
4
|
+
data.tar.gz: 0ed40e47d4b06ed40068542aed1d51502fdbf7b4651db9db4345606d4460cfad
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 75270fedc4e87641e91ae3b675b15a305a2abaf6e8ae675be96f9a62a3aec534876389e0650cb8c5b5521c5b709ce20a982ebd16c2ed3dfa3d8c0369f31468ab
|
7
|
+
data.tar.gz: 63dfa4c82044092d8bfa360a91d789dc8fb4a47f52fb532fd93095d10bbb7a3d2787efabd6081c7341869cfacdec86b0aa4b84a4fad83cd17b895af773df7684
|
checksums.yaml.gz.sig
ADDED
Binary file
|
data/.rspec
ADDED
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
3.0.3
|
data/LICENSE
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Copyright (c) 2023 Michael Cordell (https://mikecordell.com)
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
4
|
+
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
6
|
+
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# TerminalCalendar
|
2
|
+
|
3
|
+
[![CircleCI](https://dl.circleci.com/status-badge/img/gh/mcordell/terminal_calendar/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/mcordell/terminal_calendar/tree/master)
|
4
|
+
[![Coverage Status](https://coveralls.io/repos/github/mcordell/terminal_calendar/badge.svg)](https://coveralls.io/github/mcordell/terminal_calendar)
|
5
|
+
[![Gem Version](https://badge.fury.io/rb/terminal_calendar.svg)](https://badge.fury.io/rb/terminal_calendar)
|
6
|
+
|
7
|
+
> A ruby calendar and day picker for the terminal
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
Install the gem and add to the application's Gemfile by executing:
|
12
|
+
|
13
|
+
$ bundle add terminal_calendar
|
14
|
+
|
15
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
|
+
|
17
|
+
$ gem install terminal_calendar
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
`terminal_calendar` can be used to render a calendar page similar to the linux tool `cal`:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
require 'terminal_calendar'
|
25
|
+
# this will render a calendar for the current month to the comma(nd line
|
26
|
+
puts TerminalCalendar.cal
|
27
|
+
```
|
28
|
+
|
29
|
+
![cal screenshot](./_doc/cal-screenshot.png)
|
30
|
+
|
31
|
+
`terminal_calendar` can also be used as a date picker to select days from a calendar:
|
32
|
+
|
33
|
+
![picker gif](./_doc/date-picker.gif)
|
34
|
+
|
35
|
+
In the above GIF, the following code allows dates to be picked:
|
36
|
+
|
37
|
+
```ruby
|
38
|
+
require 'terminal_calendar'
|
39
|
+
|
40
|
+
dates = TerminalCalendar.date_picker
|
41
|
+
|
42
|
+
# where dates is an Array of Date objects
|
43
|
+
```
|
44
|
+
|
45
|
+
When the calendar pops up the cursor is navigated with standard error keys, `Tab` toggles a day as selected, and `Enter` will end the picking session
|
46
|
+
|
47
|
+
## Development
|
48
|
+
|
49
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
50
|
+
|
51
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
52
|
+
|
53
|
+
## Contributing
|
54
|
+
|
55
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/mcordell/terminal_calendar.
|
56
|
+
|
57
|
+
## License
|
58
|
+
|
59
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/license/mit/).
|
60
|
+
|
61
|
+
## Copyright
|
62
|
+
|
63
|
+
Copyright (c) 2023 Michael Cordell. See LICENSE for further details.
|
data/Rakefile
ADDED
Binary file
|
Binary file
|
data/certs/mcordell.pem
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
-----BEGIN CERTIFICATE-----
|
2
|
+
MIIEeDCCAuCgAwIBAgIBATANBgkqhkiG9w0BAQsFADBBMQ0wCwYDVQQDDARtaWtl
|
3
|
+
MRswGQYKCZImiZPyLGQBGRYLbWlrZWNvcmRlbGwxEzARBgoJkiaJk/IsZAEZFgNj
|
4
|
+
b20wHhcNMjMwODI2MjMzMjA0WhcNMjQwODI1MjMzMjA0WjBBMQ0wCwYDVQQDDARt
|
5
|
+
aWtlMRswGQYKCZImiZPyLGQBGRYLbWlrZWNvcmRlbGwxEzARBgoJkiaJk/IsZAEZ
|
6
|
+
FgNjb20wggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQC+VmaK9CnkHyuD
|
7
|
+
AaIbhJyccOe6ANaQFeKrI6BZ4ZpWogi96KIAhPqnjt1GZ+DJIg9tDJbSDdRThG6O
|
8
|
+
Il5jEijiBO5yQs16aVMaOlVvZLouwhCCd+6hhJFBFGzfs6jmdutBb17VFaO0jaGP
|
9
|
+
cWyoChe/cUg2B0gGmyigjexQhmHPR6nGRbJ19P/bXNkAES8UptBLR9NNyWVataaw
|
10
|
+
P8B+v+hBa8k6jDX+sRHm2wUOhotAHHmIS4bOKSGL6VX2yLwID47svyws4f0JAuqc
|
11
|
+
V9aWJgZx0NNP/mUYfbSIV9dAJeLUyPqZbB1Ba+XDyYhVYqGKcanEmDn5dFxHfLRA
|
12
|
+
cfAU/Lt8APm/i8x7y0NWoPBL0+OiRbOxCv/Nu5P32O1JUo4/MCB6Dd0y9uzbcqye
|
13
|
+
DBYNeVPYfitBm4JrJChtG9iQ6uGhx48rE8TkJ/iDgAXPIL75dJnQvsMQOKshrcs9
|
14
|
+
FpHC52pmL8lgqgBWNyNca5jyGhH1MqD9iKwiOHFJ2DHlzqFqM/0CAwEAAaN7MHkw
|
15
|
+
CQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFAuiXkk79ZpM4+khmt7l
|
16
|
+
WEP+htJ1MB8GA1UdEQQYMBaBFG1pa2VAbWlrZWNvcmRlbGwuY29tMB8GA1UdEgQY
|
17
|
+
MBaBFG1pa2VAbWlrZWNvcmRlbGwuY29tMA0GCSqGSIb3DQEBCwUAA4IBgQBIUKXJ
|
18
|
+
t8raDTY43e69HgHETL6V5AY1ExQcLGAqtqV1UqhseekAAiO804LrrSsp4FcZyDL3
|
19
|
+
EL16Emfe0/80rfQIA3fQAoazUbYm2ndjeSMza5CWDMZ8hSvSqQAu1a10wInim5ya
|
20
|
+
Q3F8I8i8fLVZk5JE0zBqJUJgtCuE7dI6v9eNcMqvA0Cubn3PMVve0vz4hArOdylr
|
21
|
+
OCoZ2UW7pZexQLyjYnokultRyAhvdGGL+stDksXaoJtcCzwfleqMsIBT35QYIc/r
|
22
|
+
Hp06A67kCqMavH70GYYej3HdahEu8CjuwGMykA/jww0F+p60QvoGZOcg3hkQRUdT
|
23
|
+
oD3LQJLiK+oNexJ0i/rh1pbCMAq1sjGFGyv2ACBn+KEmmLzkDgldJfDyyOtOgomB
|
24
|
+
sHCLzDW9qhsvbL8U+XYT0o7iwdtzH6ql3vTd5mxENXSOC9rVMI2amNAJkuwpJvCA
|
25
|
+
ZDG8uvZdWY9MZGDjweK0/9rt/PItld9RW4EQJxwgyu/yU1giVs/0wZib+Uw=
|
26
|
+
-----END CERTIFICATE-----
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
# Useful extensions copied from ActiveSupport::CoreExt
|
5
|
+
module DateExtensions
|
6
|
+
COMMON_YEAR_DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31].freeze
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def days_in_month(month, year)
|
10
|
+
if month == 2 && ::Date.gregorian_leap?(year)
|
11
|
+
29
|
12
|
+
else
|
13
|
+
COMMON_YEAR_DAYS_IN_MONTH[month]
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.included(base)
|
19
|
+
base.extend(ClassMethods)
|
20
|
+
end
|
21
|
+
|
22
|
+
def beginning_of_month
|
23
|
+
self.class.new(
|
24
|
+
year,
|
25
|
+
month,
|
26
|
+
1
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def end_of_month
|
31
|
+
self.class.new(
|
32
|
+
year,
|
33
|
+
month,
|
34
|
+
self.class.days_in_month(month, year)
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
Date.include(DateExtensions)
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class TerminalCalendar
|
4
|
+
class DatePicker
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def self.pick(month: TerminalCalendar::Month.this_month)
|
8
|
+
new(month).pick
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :month, :reader, :cursor
|
12
|
+
|
13
|
+
# @return [TerminalCalendar::Selection::Selector]
|
14
|
+
# @api private
|
15
|
+
attr_reader :selector
|
16
|
+
|
17
|
+
def initialize(month, input: $stdin, output: $stdout, env: ENV, interrupt: :error,
|
18
|
+
track_history: true)
|
19
|
+
@current_page = Selection::MonthPage.build(month)
|
20
|
+
@month_pages = [[month, @current_page]].to_h
|
21
|
+
@reader = TTY::Reader.new(
|
22
|
+
input: input,
|
23
|
+
output: output,
|
24
|
+
interrupt: interrupt,
|
25
|
+
track_history: track_history,
|
26
|
+
env: env
|
27
|
+
)
|
28
|
+
@output = output
|
29
|
+
@cursor = TTY::Cursor
|
30
|
+
@pastel = Pastel.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def pick
|
34
|
+
@output.print(@cursor.hide)
|
35
|
+
render
|
36
|
+
|
37
|
+
selection_loop
|
38
|
+
|
39
|
+
month_pages.values.flat_map { |p| p.selection_grid.selected_cells.map(&:date) }
|
40
|
+
ensure
|
41
|
+
@output.print(@cursor.show)
|
42
|
+
end
|
43
|
+
|
44
|
+
def render
|
45
|
+
@output.print(current_page.render)
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
# rubocop:disable Metrics/MethodLength
|
51
|
+
def selection_loop
|
52
|
+
loop do
|
53
|
+
press = reader.read_keypress
|
54
|
+
kp = TTY::Reader::Keys.keys.fetch(press) { press }
|
55
|
+
|
56
|
+
case kp
|
57
|
+
when :up, :down, :left, :right
|
58
|
+
move(kp)
|
59
|
+
redraw
|
60
|
+
when :tab
|
61
|
+
toggle!
|
62
|
+
when :return
|
63
|
+
unless selector&.on_header?
|
64
|
+
break
|
65
|
+
end
|
66
|
+
|
67
|
+
select_month
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
# rubocop:enable Metrics/MethodLength
|
72
|
+
|
73
|
+
def toggle!
|
74
|
+
return if selector&.on_header?
|
75
|
+
|
76
|
+
selector&.toggle_selected!
|
77
|
+
redraw
|
78
|
+
end
|
79
|
+
|
80
|
+
def select_month
|
81
|
+
clear_page_lines
|
82
|
+
new_date = TerminalCalendar::Selection::MonthYearDialog.new(
|
83
|
+
output: @output,
|
84
|
+
start_at: current_page.month.start_of_month
|
85
|
+
).select
|
86
|
+
new_month = TerminalCalendar::Month.new(new_date.month, new_date.year)
|
87
|
+
set_new_page(new_month)
|
88
|
+
clear_selection_dialog
|
89
|
+
initialize_selector(:bottom)
|
90
|
+
render
|
91
|
+
@output.print(@cursor.hide)
|
92
|
+
redraw
|
93
|
+
end
|
94
|
+
|
95
|
+
def clear_full_page!
|
96
|
+
@output.print(refresh(current_page.line_count))
|
97
|
+
end
|
98
|
+
|
99
|
+
def clear_page_lines
|
100
|
+
@output.print(refresh(current_page.redraw_lines.length))
|
101
|
+
end
|
102
|
+
|
103
|
+
def clear_selection_dialog
|
104
|
+
@output.print(refresh(2))
|
105
|
+
end
|
106
|
+
|
107
|
+
attr_reader :current_page, :month_pages
|
108
|
+
|
109
|
+
def_delegators(:current_page, :selection_grid)
|
110
|
+
|
111
|
+
def redraw
|
112
|
+
lines = current_page.redraw_lines
|
113
|
+
@output.print(refresh(lines.length) + lines.join("\n"))
|
114
|
+
end
|
115
|
+
|
116
|
+
def refresh(lines)
|
117
|
+
@cursor.clear_lines(lines)
|
118
|
+
end
|
119
|
+
|
120
|
+
# Moves the selector in the specified direction.
|
121
|
+
# @param direction [Symbol] The direction to move the selector in.
|
122
|
+
# Valid values are :up, :down, :left, and :right.
|
123
|
+
#
|
124
|
+
# @return [TerminalCalendar::Selection::Selector]
|
125
|
+
def move(direction)
|
126
|
+
return initialize_selector(direction) unless selector
|
127
|
+
|
128
|
+
case selector.move(direction)
|
129
|
+
when :off_left
|
130
|
+
new_month = @current_page.month.previous_month
|
131
|
+
set_new_page(new_month)
|
132
|
+
when :off_right
|
133
|
+
new_month = @current_page.month.next_month
|
134
|
+
set_new_page(new_month)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def set_new_page(new_month)
|
139
|
+
clear_full_page!
|
140
|
+
@current_page = month_pages.fetch(new_month) do
|
141
|
+
month_pages[new_month] = Selection::MonthPage.build(new_month)
|
142
|
+
end
|
143
|
+
initialize_selector(:bottom)
|
144
|
+
render
|
145
|
+
end
|
146
|
+
|
147
|
+
# Initializes the selector based on the given direction.
|
148
|
+
#
|
149
|
+
# @param direction [Symbol] The direction to initialize the selector.
|
150
|
+
# Must be one of :up, :left, :down, or :right.
|
151
|
+
#
|
152
|
+
# @return [TerminalCalendar::Selection::Selector]
|
153
|
+
#
|
154
|
+
# @api private
|
155
|
+
def initialize_selector(direction)
|
156
|
+
position = %i(up left).include?(direction) ? :bottom : :top
|
157
|
+
@selector = TerminalCalendar::Selection::Selector.build(current_page, position)
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class TerminalCalendar
|
3
|
+
class Month
|
4
|
+
class CalendarDay
|
5
|
+
# @return [Pastel] The pastel object to use for decorating text
|
6
|
+
attr_reader :pastel
|
7
|
+
|
8
|
+
# @return [Date] The date for this calendar day
|
9
|
+
attr_reader :date
|
10
|
+
|
11
|
+
# @param date [Date] The date to be assigned to this calendar day
|
12
|
+
# @param pastel [Pastel] The pastel object to use for decorating text
|
13
|
+
def initialize(date, pastel=Pastel.new)
|
14
|
+
@date = date
|
15
|
+
@pastel = pastel
|
16
|
+
end
|
17
|
+
|
18
|
+
# Renders the day as a string.
|
19
|
+
# @return [String] the day as a string
|
20
|
+
def render
|
21
|
+
as_string = day.to_s
|
22
|
+
as_string = " #{as_string}" if as_string.length == 1
|
23
|
+
as_string = pastel.red(as_string) if today?
|
24
|
+
as_string
|
25
|
+
end
|
26
|
+
|
27
|
+
alias to_s render
|
28
|
+
|
29
|
+
# Determines if the given date is today.
|
30
|
+
#
|
31
|
+
# @return [Boolean] Returns true if the date is today, false otherwise.
|
32
|
+
def today?
|
33
|
+
date == Date.today
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns whether the object is null or not.
|
37
|
+
#
|
38
|
+
# @return [false] Returns false.
|
39
|
+
def null?
|
40
|
+
false
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the day of the date.
|
44
|
+
#
|
45
|
+
# @return [Integer] The day of the date.
|
46
|
+
def day
|
47
|
+
date.day
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
class NullDay < CalendarDay
|
52
|
+
def initialize
|
53
|
+
super(nil)
|
54
|
+
end
|
55
|
+
|
56
|
+
def render
|
57
|
+
' '
|
58
|
+
end
|
59
|
+
alias to_s render
|
60
|
+
|
61
|
+
# Returns whether the object is null or not.
|
62
|
+
#
|
63
|
+
# @return [true] Returns true.
|
64
|
+
def null?
|
65
|
+
true
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require 'terminal_calendar/month/calendar_day'
|
3
|
+
|
4
|
+
class TerminalCalendar
|
5
|
+
class Month
|
6
|
+
# Returns an array of rows representing the data.
|
7
|
+
# @return [Array<Array>] an array of arrays, where each inner array represents a row of data
|
8
|
+
attr_reader :as_rows
|
9
|
+
|
10
|
+
attr_reader :month, :year, :start_of_month, :end_of_month
|
11
|
+
|
12
|
+
DAYS_IN_THE_WEEK = 7
|
13
|
+
|
14
|
+
def self.this_month
|
15
|
+
new(Date.today.month, Date.today.year)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.new(*arguments, &block)
|
19
|
+
month = arguments[0].to_i
|
20
|
+
fail ArgumentError.new('Month number must be 1-12') unless month >= 1 && month <= 12
|
21
|
+
|
22
|
+
year = arguments[1].to_i
|
23
|
+
key = [year, month]
|
24
|
+
|
25
|
+
TerminalCalendar.all_months.fetch(key) do
|
26
|
+
instance = allocate
|
27
|
+
instance.send(:initialize, *arguments, &block)
|
28
|
+
TerminalCalendar.all_months[key] = instance.freeze
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(month, year)
|
33
|
+
@month = month.to_i
|
34
|
+
@year = year.to_i
|
35
|
+
@start_of_month = Date.new(year, month, 1)
|
36
|
+
@end_of_month = @start_of_month.end_of_month
|
37
|
+
@as_rows ||= build_rows
|
38
|
+
end
|
39
|
+
|
40
|
+
def next_month
|
41
|
+
new_month = month == 12 ? 1 : month + 1
|
42
|
+
new_year = new_month == 1 ? year + 1 : year
|
43
|
+
self.class.new(new_month, new_year)
|
44
|
+
end
|
45
|
+
|
46
|
+
def previous_month
|
47
|
+
new_month = month == 1 ? 12 : month - 1
|
48
|
+
new_year = new_month == 12 ? year - 1 : year
|
49
|
+
self.class.new(new_month, new_year)
|
50
|
+
end
|
51
|
+
|
52
|
+
def ==(other)
|
53
|
+
eql?(other)
|
54
|
+
end
|
55
|
+
|
56
|
+
def eql?(other)
|
57
|
+
other.month == month && other.year == year
|
58
|
+
end
|
59
|
+
|
60
|
+
def hash
|
61
|
+
[month, year].hash
|
62
|
+
end
|
63
|
+
|
64
|
+
def render
|
65
|
+
TerminalCalendar::Selection::MonthPage.new(self).render
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
# rubocop:disable Metrics/AbcSize
|
71
|
+
def build_rows
|
72
|
+
current_row = empty_week
|
73
|
+
[].tap do |rows|
|
74
|
+
(start_of_month..end_of_month).each do |d|
|
75
|
+
if d.wday.zero? && !current_row.empty?
|
76
|
+
rows.push(current_row)
|
77
|
+
current_row = empty_week
|
78
|
+
end
|
79
|
+
current_row[d.wday] = CalendarDay.new(d, pastel).freeze
|
80
|
+
end
|
81
|
+
rows.push(current_row) unless current_row.empty?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
# rubocop:enable Metrics/AbcSize
|
85
|
+
|
86
|
+
def null_date
|
87
|
+
@null_date ||= NullDay.new.freeze
|
88
|
+
end
|
89
|
+
|
90
|
+
def empty_week
|
91
|
+
Array.new(DAYS_IN_THE_WEEK) { null_date }
|
92
|
+
end
|
93
|
+
|
94
|
+
def pastel
|
95
|
+
@pastel ||= Pastel.new(enabled: true)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
class TerminalCalendar
|
3
|
+
module Selection
|
4
|
+
DEFAULT_SELECTED_STYLE = 'XX'
|
5
|
+
|
6
|
+
class Cell
|
7
|
+
# @return [Object] the object that this cell wraps
|
8
|
+
attr_reader(:underlying_object)
|
9
|
+
# @return [Boolean] whether this cell is currently selected
|
10
|
+
attr_reader(:selected)
|
11
|
+
# @return [String] the rendered style of a cell that is selected
|
12
|
+
attr_reader(:selected_style)
|
13
|
+
|
14
|
+
# Builds a new cell object based on the given object. If the
|
15
|
+
# underlying object is Null, a NullCell will be returned
|
16
|
+
# @param obj [Object] The object to build the cell from.
|
17
|
+
# @return [Cell] The newly built cell object.
|
18
|
+
def self.build(obj)
|
19
|
+
obj.null? ? NullCell.new(obj) : new(obj)
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(underlying_object, selected: false, selected_style: DEFAULT_SELECTED_STYLE)
|
23
|
+
@underlying_object = underlying_object
|
24
|
+
@selected = selected
|
25
|
+
@selected_style = selected_style
|
26
|
+
end
|
27
|
+
|
28
|
+
# Renders the selected_style string if it is selected, otherwise
|
29
|
+
# returns the result of rendering the underlying object.
|
30
|
+
#
|
31
|
+
# @return [String] The cell's rendered content
|
32
|
+
def render
|
33
|
+
return underlying_object.render unless selected?
|
34
|
+
|
35
|
+
selected_style
|
36
|
+
end
|
37
|
+
|
38
|
+
# Checks if the object is null.
|
39
|
+
#
|
40
|
+
# @return [Boolean] Returns false.
|
41
|
+
def null?
|
42
|
+
false
|
43
|
+
end
|
44
|
+
|
45
|
+
# Toggles the selected state of the object.
|
46
|
+
# @return [Boolean] the new selected state of the object
|
47
|
+
def toggle_selected!
|
48
|
+
@selected = !@selected
|
49
|
+
end
|
50
|
+
|
51
|
+
alias_method :selected?, :selected
|
52
|
+
|
53
|
+
# Calls the missing method on the underlying object.
|
54
|
+
#
|
55
|
+
# @param method [Symbol] the name of the missing method
|
56
|
+
# @param args [Array] the arguments passed to the missing method
|
57
|
+
# @param block [Proc] the block passed to the missing method
|
58
|
+
# @return [Object] the result of calling the missing method on the underlying object
|
59
|
+
# @raise [NoMethodError] if the underlying object does not respond to the missing method
|
60
|
+
def method_missing(method, *args, &block)
|
61
|
+
underlying_object.send(method, *args, &block)
|
62
|
+
end
|
63
|
+
|
64
|
+
# Checks if the underlying object responds to the missing method.
|
65
|
+
#
|
66
|
+
# @param method [Symbol] the name of the missing method
|
67
|
+
# @param include_all [Boolean] whether to include private methods in the check
|
68
|
+
# @return [Boolean] true if the underlying object responds to the missing method, super otherwise
|
69
|
+
def respond_to_missing?(method, include_all)
|
70
|
+
underlying_object.respond_to?(method) || super
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|