terminal-notes 0.0.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3a0256e2d1947621a55db84286b5a621e6acc0711a0861b41ca95b04622871c6
4
+ data.tar.gz: b9af1e67c2f847cb0b2c56e889ae4e31cf5a63ccd1185972d045d017f0cf070c
5
+ SHA512:
6
+ metadata.gz: 98dda1f386c2e92b67d9cf81fb5132c0a2388ab4fbd802482000578f050836ab06a816d14ee0a3f461bf6c79a1c5a5829b372e36c7ad9c79a34b7440caafa0a8
7
+ data.tar.gz: aa8dfc344c7618dd2190398b2d253946e4d5fa4386d7244dfd29d16e148baf3b6e9deab51cf3624d309f4d1d7134c747d9f855eca9b86a1ece2c56be02a427a7
@@ -0,0 +1,69 @@
1
+ # Note Transpose
2
+ [![Gem Version](https://badge.fury.io/rb/terminal-notes.svg)](https://badge.fury.io/rb/terminal-notes)
3
+ [![Build Status](https://travis-ci.org/vyder/terminal-notes.svg?branch=master)](https://travis-ci.org/vyder/terminal-notes)
4
+ [![Inline docs](http://inch-ci.org/github/vyder/terminal-notes.svg?branch=master)](http://inch-ci.org/github/vyder/terminal-notes)
5
+
6
+ Searchable notes in your terminal! What's not to like?
7
+
8
+ ## Table of Contents
9
+
10
+ - [Install](#install)
11
+ - [Usage](#usage)
12
+ - [Documentation](#documentation)
13
+ - [Roadmap](#roadmap)
14
+ - [Support](#support)
15
+ - [Contributing](#contributing)
16
+
17
+ ## Install
18
+
19
+ ❯ gem install terminal-notes
20
+
21
+ On first run, you will be asked to confirm the destination of your install. It defaults to `~/.notesrc/`.
22
+
23
+ The main configuration file is located at `~/.notesrc/config`. It's a YAML file.
24
+
25
+ All your notes are saved in `~/.notesrc/db` as text files.
26
+
27
+ Additionally, I like to symlink the binary to my `~/bin` folder for ease of access. You can do that with:
28
+
29
+ ❯ ln -sn $(which terminal-notes) ~/bin/notes
30
+
31
+ Make sure you have your `~/bin` in your `PATH` for this to work.
32
+
33
+ ## Usage
34
+
35
+ ❯ terminal-notes
36
+
37
+ TODO: Write this section
38
+
39
+ ## Documentation
40
+
41
+ You can find the documentation [here](https://vyder.github.io/terminal-notes/)
42
+
43
+ ## Roadmap
44
+
45
+ Here is my planned roadmap:
46
+
47
+ (Last updated Aug 26th, 2020)
48
+
49
+ - [ ] Draw a shortcuts info bar like `nano` has
50
+ - [ ] Draw a title bar at the top
51
+ - [ ] Implement `.notesrc` and database
52
+ - [ ] Create an install flow (as described in the README)
53
+ - [ ] Implement responsive layout
54
+ - [ ] Implement a better file matcher
55
+ - [ ] Create a non fancy mode which works better in smaller terminal screens
56
+ - [ ] Update status line to display:
57
+ - Matcher
58
+ - [ ] Abstract out UI work to Layout module that is stateful and tracks x,y widget positions
59
+
60
+ Future:
61
+ - Implement file previews with `tty-markdown` when in full screen mode
62
+
63
+ ## Support
64
+
65
+ Please [open an issue](https://github.com/vyder/terminal-notes/issues/new) for support.
66
+
67
+ ## Contributing
68
+
69
+ Please contribute using [Github Flow](https://guides.github.com/introduction/flow/). Create a branch, add commits, and [open a pull request](https://github.com/vyder/terminal-notes/compare/).
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'terminal-notes'
4
+
5
+ unless ARGV.size == 1
6
+ puts "usage: #{$0} folder"
7
+ exit
8
+ end
9
+
10
+ # TODO: Parse a ~/.notesrc
11
+ config = {
12
+ directory: ARGV[0]
13
+ }
14
+
15
+ app = TerminalNotes::App.new
16
+ app.start(config)
@@ -0,0 +1,175 @@
1
+ require_relative 'terminal-notes/rurses'
2
+
3
+ require_relative 'terminal-notes/cursor'
4
+ require_relative 'terminal-notes/widget'
5
+ require_relative 'terminal-notes/text_field'
6
+ require_relative 'terminal-notes/search_field'
7
+ require_relative 'terminal-notes/file_list'
8
+ require_relative 'terminal-notes/info_bar'
9
+
10
+ module TerminalNotes
11
+ class App
12
+ @has_initialized = false
13
+
14
+ def start(config)
15
+ directory = config[:directory]
16
+
17
+ Rurses.program(modes: %i[c_break no_echo keypad]) do |screen|
18
+ @screen = screen
19
+
20
+ @widgets = []
21
+
22
+ @search_field = SearchField.new(parent: @screen)
23
+ @widgets << @search_field
24
+
25
+ @info_bar = InfoBar.new(parent: @screen) { get_context }
26
+ @widgets << @info_bar
27
+
28
+ padding = 2
29
+
30
+ # file_list_height = screen.size[:lines]
31
+ # - @search_field.height
32
+ # - @info_bar.height
33
+ # - padding
34
+ file_list_height = 40
35
+ @file_list = FileList.new(parent: @screen,
36
+ height: file_list_height,
37
+ y: @search_field.height + padding,
38
+ directory: directory)
39
+ @widgets << @file_list
40
+
41
+ # Register the text_field filter as a delegate
42
+ # of the search field
43
+ @search_field.on_text_changed do |pattern|
44
+ @file_list.filter(pattern)
45
+ end
46
+
47
+ # Register file open handler
48
+ @file_list.on_file_opened do |file|
49
+ open_file(file)
50
+ end
51
+
52
+ # A PanelStack handles how windows are drawn
53
+ # over each other - i.e. overlapping
54
+ #
55
+ # I'm not really using this ability, however, in
56
+ # Rurses, the PanelStack seems to be the only way
57
+ # to delete/deallocate a window completely
58
+ #
59
+ @panels = Rurses::PanelStack.new
60
+ @widgets.each { |widget| @panels.add(widget.window) }
61
+ @panels.refresh_in_memory
62
+
63
+ # Events
64
+ # watch_for_resize
65
+
66
+ # Set initial state
67
+ set_context :search
68
+
69
+ # Actually draw the screen
70
+ # @panels.refresh_in_memory
71
+ Rurses.update_screen
72
+
73
+ @has_initialized = true
74
+
75
+ loop do
76
+ key = Rurses.get_key
77
+
78
+ if @context == :browse
79
+ case key
80
+ when "q", :CTRL_X
81
+ break # quit
82
+ when "m"
83
+ @file_list.toggle_matcher
84
+ @file_list.filter(@search_field.text)
85
+
86
+ when "\t", "/"
87
+ set_context :search
88
+ else
89
+ @file_list.on_key(key)
90
+ end
91
+ else
92
+ case key
93
+ when :CTRL_X
94
+ break # quit
95
+ when "\t", :ENTER
96
+ set_context :browse
97
+ else
98
+ @search_field.on_key(key)
99
+ end
100
+ end
101
+
102
+ # When all the downstream widgets are done updating
103
+ # in-memory, actually redraw the screen
104
+ # @panels.refresh_in_memory
105
+ Rurses.update_screen
106
+ end
107
+
108
+ @screen.move_cursor(x: 0, y: 0)
109
+ end
110
+ end
111
+
112
+ def set_context context
113
+ @context = context
114
+
115
+ if @context == :browse
116
+ Rurses.curses.curs_set(0)
117
+ @search_field.unfocus
118
+ @file_list.focus
119
+
120
+ elsif @context == :search
121
+ Rurses.curses.curs_set(1)
122
+ @file_list.unfocus
123
+ @search_field.focus
124
+
125
+ end
126
+
127
+ @info_bar.draw
128
+ end
129
+
130
+ def get_context
131
+ { mode: @context }
132
+ end
133
+
134
+ def open_file(file)
135
+ system(editor, file)
136
+ redraw
137
+
138
+ # Hack to redraw focus correctly
139
+ # TODO: Figure out why this works
140
+ set_context :search
141
+ set_context :browse
142
+ end
143
+
144
+ def editor
145
+ ENV['EDITOR'] || DEFAULT_EDITOR
146
+ end
147
+
148
+ # This method can be invoked independent of the wait for
149
+ # keyboard input loop, so explicitly call for a screen
150
+ # update after widgets have been drawn
151
+ def redraw(resize: false)
152
+ return unless @has_initialized
153
+
154
+ @widgets.each(&:redraw)
155
+ # @widgets.each do |widget|
156
+ # widget.window.clear
157
+ # @panels.remove(widget.window)
158
+ # widget.draw(resize: resize)
159
+ # @panels.add(widget.window)
160
+ # end
161
+
162
+ set_context @context
163
+
164
+ @panels.refresh_in_memory
165
+ Rurses.update_screen
166
+ end
167
+
168
+ def watch_for_resize
169
+ # SIGWINCH
170
+ # The SIGWINCH signal is sent to a process when its
171
+ # controlling terminal changes its size (a window change).
172
+ Signal.trap(:SIGWINCH) { redraw(resize: true) }
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,26 @@
1
+ module TerminalNotes
2
+ class Cursor
3
+ attr_reader :x, :y
4
+
5
+ def initialize(x: 0, y: 0)
6
+ @x = x.to_i
7
+ @y = y.to_i
8
+ end
9
+
10
+ def moveBy(deltaX = 0, deltaY = 0)
11
+ @x += deltaX
12
+ @y += deltaY
13
+ self
14
+ end
15
+
16
+ def moveTo(x: nil, y: nil)
17
+ @x = x unless x.nil?
18
+ @y = y unless y.nil?
19
+ self
20
+ end
21
+
22
+ def to_hash
23
+ { x: @x, y: @y }
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,217 @@
1
+ require 'fuzzy_match'
2
+
3
+ module TerminalNotes
4
+ class FileList < Widget
5
+ WIDTH = 1
6
+
7
+ EXTENSIONS = ['.md', '.txt']
8
+ DEFAULT_EDITOR = "nano"
9
+
10
+ CONTENT_PADDING = 2
11
+ FILE_INDICATOR = "❯ "
12
+ INTER_COL_GAP = " "
13
+
14
+ MATCHERS = [:regex, :fuzzy]
15
+
16
+ def initialize(parent:, height:, y:, directory:,
17
+ extensions: EXTENSIONS)
18
+ super(parent: parent, title: "Files",
19
+ width: WIDTH, height: height, y: y)
20
+
21
+ @content_start = Cursor.new(x: 0, y: CONTENT_PADDING)
22
+
23
+ # Find all the files that match specified extensions
24
+ @all_files = Dir.glob("#{directory}/*")
25
+ @all_files = @all_files.find_all do |f|
26
+ f.match /(#{EXTENSIONS.join("|")})$/
27
+ end
28
+ @all_files = @all_files.map do |f|
29
+ {
30
+ name: f.gsub("#{directory}/", ''),
31
+ path: f,
32
+ date: "Jun 24, 2012",
33
+ time: "05:32pm"
34
+ }
35
+ end
36
+ @filtered_files = @all_files
37
+
38
+ @fuzzy_matcher = FuzzyMatch.new(@all_files, read: :name)
39
+ @curr_matcher = 0
40
+
41
+ @current_line = 0
42
+ @has_focus = false
43
+
44
+ @on_file_opened_delegates = []
45
+
46
+ draw
47
+ end
48
+
49
+ # TODO
50
+ def focus
51
+ @has_focus = true
52
+ draw
53
+ end
54
+
55
+ def unfocus
56
+ @has_focus = false
57
+ draw
58
+ end
59
+
60
+ def draw
61
+ super do
62
+ # Calculate where to start drawing the table
63
+ #
64
+ table_width = 0
65
+
66
+ # Calculate NAME col width
67
+ longest_file = @all_files.max { |a, b| a[:name].size <=> b[:name].size }
68
+ if longest_file.nil?
69
+ name_col_width = 10
70
+ else
71
+ name_col_width = 2 + longest_file[:name].size
72
+ end
73
+
74
+ # Calculate DATE col width, e.g. Jun 24, 2012
75
+ date_col_width = 12
76
+
77
+ # Calculate TIME col width, e.g. 05:24pm
78
+ time_col_width = 7
79
+
80
+ table_width += name_col_width + INTER_COL_GAP.size
81
+ table_width += date_col_width + INTER_COL_GAP.size
82
+ table_width += time_col_width
83
+
84
+ # Move cursor to center the table
85
+ cursor = @content_start.dup
86
+ cursor.moveTo(x: (@parent.size[:columns] - table_width) / 2)
87
+ @window.move_cursor(cursor)
88
+
89
+ # Start actually drawing
90
+ #
91
+
92
+ # Draw header
93
+ line = "NAME".ljust(name_col_width) + INTER_COL_GAP
94
+ line += "DATE".ljust(date_col_width) + INTER_COL_GAP
95
+ line += "TIME".ljust(time_col_width)
96
+ @window.draw_string(line)
97
+
98
+ # Move cursor back to accomodate the file indicator
99
+ cursor.moveBy(-FILE_INDICATOR.size, 1)
100
+ @window.move_cursor(cursor)
101
+
102
+ no_indicator = " " * FILE_INDICATOR.size
103
+
104
+ index = 0
105
+ @filtered_files.each do |file|
106
+ line = no_indicator
107
+ line += file[:name].ljust(name_col_width) + INTER_COL_GAP
108
+ line += file[:date].rjust(date_col_width) + INTER_COL_GAP
109
+ line += file[:time].rjust(time_col_width)
110
+
111
+ @window.draw_string(line)
112
+
113
+ # Draw the arrow
114
+ if @has_focus && index == @current_line
115
+ @window.move_cursor(cursor)
116
+ @window.draw_string(FILE_INDICATOR)
117
+ end
118
+
119
+ cursor.moveBy(0, 1)
120
+ @window.move_cursor(cursor)
121
+
122
+ index += 1
123
+ end
124
+
125
+ clear_line = no_indicator + " " * table_width
126
+ while index < @all_files.size
127
+ @window.draw_string(clear_line)
128
+ cursor.moveBy(0, 1)
129
+ @window.move_cursor(cursor)
130
+
131
+ index += 1
132
+ end
133
+
134
+ end
135
+ end
136
+
137
+ def on_key key
138
+ return unless @has_focus
139
+
140
+ case key
141
+ when "j"
142
+ scroll_down
143
+ when "k"
144
+ scroll_up
145
+ when :ENTER
146
+ open_current
147
+ else
148
+ end
149
+
150
+ draw
151
+ end
152
+
153
+ def filter pattern
154
+ old_file_list = @filtered_files
155
+
156
+ if pattern.empty?
157
+ @filtered_files = @all_files
158
+ else
159
+ matcher_type = MATCHERS[@curr_matcher]
160
+ case matcher_type
161
+ when :regex
162
+ @filtered_files = @all_files.find_all do |f|
163
+ f[:name].match /#{pattern}/
164
+ end
165
+ when :fuzzy
166
+ @filtered_files = @fuzzy_matcher.find_all(pattern)
167
+ else
168
+ raise "Unknown Matcher! '#{matcher_type}'"
169
+ end
170
+ end
171
+
172
+ # If the file list has changed, reset current line marker
173
+ @current_line = 0 unless old_file_list == @filtered_files
174
+
175
+ draw
176
+ end
177
+
178
+ def toggle_matcher
179
+ @curr_matcher += 1
180
+ @curr_matcher = 0 if @curr_matcher >= MATCHERS.size
181
+ end
182
+
183
+ def on_file_opened &block
184
+ @on_file_opened_delegates << block
185
+ end
186
+
187
+ def notify_file_opened(file)
188
+ @on_file_opened_delegates.each do |delegate|
189
+ delegate.call(file)
190
+ end
191
+ end
192
+
193
+ private
194
+
195
+ def open_current
196
+ file = @filtered_files[@current_line]
197
+ notify_file_opened(file[:path])
198
+ end
199
+
200
+ def editor
201
+ # %x(echo $EDITOR).chomp || DEFAULT_EDITOR
202
+ ENV['EDITOR'] || DEFAULT_EDITOR
203
+ end
204
+
205
+ def scroll_down
206
+ if @current_line < (@filtered_files.size - 1)
207
+ @current_line += 1
208
+ end
209
+ end
210
+
211
+ def scroll_up
212
+ if @current_line > 0
213
+ @current_line -= 1
214
+ end
215
+ end
216
+ end
217
+ end
@@ -0,0 +1,25 @@
1
+ module TerminalNotes
2
+ class InfoBar < Widget
3
+ HEIGHT = 3
4
+
5
+ def initialize(config: {}, parent:, &block)
6
+ super(parent: parent, title: "", height: HEIGHT,
7
+ y: (parent.size[:lines] - HEIGHT), border: false)
8
+
9
+ @get_context = block
10
+ draw
11
+ end
12
+
13
+ def draw
14
+ super do
15
+ context = @get_context.call
16
+ title = "Mode: #{context[:mode].to_s.capitalize}"
17
+
18
+ cursor = Cursor.new(x: (@parent.size[:columns] - title.size) / 2,
19
+ y: 0)
20
+ @window.move_cursor(cursor)
21
+ @window.draw_string(title)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,66 @@
1
+ require "ffi-ncurses"
2
+
3
+ require_relative "rurses/version"
4
+ require_relative "rurses/panel_stack"
5
+ require_relative "rurses/window"
6
+
7
+ module Rurses
8
+ SPECIAL_KEYS = Hash[
9
+ FFI::NCurses::KeyDefs
10
+ .constants
11
+ .select { |name| name.to_s.start_with?("KEY_") }
12
+ .map { |name|
13
+ [ FFI::NCurses::KeyDefs.const_get(name),
14
+ name.to_s.sub(/\AKEY_/, "").to_sym ]
15
+ }
16
+ ]
17
+
18
+ module_function
19
+
20
+ def curses
21
+ FFI::NCurses
22
+ end
23
+
24
+ def program(modes: [ ])
25
+ @stdscr = Window.new(curses_ref: curses.initscr, standard_screen: true)
26
+ @stdscr.change_modes(modes)
27
+ yield(@stdscr)
28
+ ensure
29
+ curses.endwin
30
+ end
31
+
32
+ def stdscr
33
+ @stdscr
34
+ end
35
+
36
+ def get_key
37
+ case (char = curses.getch)
38
+ when curses::KeyDefs::KEY_CODE_YES..curses::KeyDefs::KEY_MAX
39
+ SPECIAL_KEYS[char]
40
+ when curses::ERR
41
+ nil
42
+ when 1
43
+ :CTRL_A
44
+ when 5
45
+ :CTRL_E
46
+ when 10
47
+ :ENTER
48
+ when 11
49
+ :CTRL_K
50
+ when 14
51
+ :CTRL_N
52
+ when 23
53
+ :CTRL_W
54
+ when 24
55
+ :CTRL_X
56
+ when 127
57
+ :BACKSPACE
58
+ else
59
+ char.chr
60
+ end
61
+ end
62
+
63
+ def update_screen
64
+ curses.doupdate
65
+ end
66
+ end
@@ -0,0 +1,35 @@
1
+ module Rurses
2
+ class PanelStack
3
+ def initialize
4
+ @window_to_panel_map = { }
5
+ end
6
+
7
+ attr_reader :window_to_panel_map
8
+ private :window_to_panel_map
9
+
10
+ def add(window, add_subwindows: true)
11
+ window_to_panel_map[window] = Rurses.curses.new_panel(window.curses_ref)
12
+ if add_subwindows
13
+ window.subwindows.each_value do |subwindow|
14
+ add(subwindow, add_subwindows: add_subwindows)
15
+ end
16
+ end
17
+ end
18
+ alias_method :<<, :add
19
+
20
+ def remove(window, remove_subwindows: true)
21
+ if remove_subwindows
22
+ window.subwindows.each_value do |subwindow|
23
+ remove(subwindow, remove_subwindows: remove_subwindows)
24
+ end
25
+ end
26
+ window.clear
27
+ Rurses.curses.del_panel(window_to_panel_map[window])
28
+ Rurses.curses.delwin(window.curses_ref)
29
+ end
30
+
31
+ def refresh_in_memory
32
+ Rurses.curses.update_panels
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,3 @@
1
+ module Rurses
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,148 @@
1
+ module Rurses
2
+ class Window
3
+ MODE_NAMES = {
4
+ c_break: :cbreak,
5
+ no_echo: :noecho,
6
+ keypad: [:keypad, :window, true],
7
+ hide_cursor: [:curs_set, 0],
8
+ non_blocking: [:timeout, 0]
9
+ }
10
+ ATTRIBUTES = Hash.new { |all, name|
11
+ all[name] = Rurses.curses.const_get("A_#{name.upcase}")
12
+ }
13
+
14
+ def initialize(**details)
15
+ @curses_ref = details.fetch(:curses_ref) {
16
+ Rurses.curses.newwin(
17
+ details.fetch(:lines),
18
+ details.fetch(:columns),
19
+ details.fetch(:y),
20
+ details.fetch(:x)
21
+ )
22
+ }
23
+ @standard_screen = details.fetch(:standard_screen) { false }
24
+ @subwindows = { }
25
+ end
26
+
27
+ attr_reader :curses_ref, :subwindows
28
+
29
+ def standard_screen?
30
+ @standard_screen
31
+ end
32
+
33
+ def cursor_x
34
+ Rurses.curses.getcurx(curses_ref)
35
+ end
36
+
37
+ def cursor_y
38
+ Rurses.curses.getcury(curses_ref)
39
+ end
40
+
41
+ def cursor_xy
42
+ y, x = Rurses.curses.getyx(curses_ref)
43
+ {x: x, y: y}
44
+ end
45
+
46
+ def columns
47
+ Rurses.curses.getmaxx(curses_ref)
48
+ end
49
+
50
+ def lines
51
+ Rurses.curses.getmaxy(curses_ref)
52
+ end
53
+
54
+ def size
55
+ {columns: columns, lines: lines}
56
+ end
57
+
58
+ def resize(lines: , columns: )
59
+ Rurses.curses.resizeterm(lines, columns) if standard_screen?
60
+ Rurses.curses.wresize(curses_ref, lines, columns)
61
+ end
62
+
63
+ def change_modes(modes)
64
+ modes.each do |name|
65
+ mode = Array(MODE_NAMES[name] || name)
66
+ Rurses.curses.send(*mode.map { |arg| arg == :window ? curses_ref : arg })
67
+ end
68
+ end
69
+
70
+ def draw_border( left: 0, right: 0, top: 0, bottom: 0,
71
+ top_left: 0, top_right: 0, bottom_left: 0, bottom_right: 0 )
72
+ args = [
73
+ left, right, top, bottom,
74
+ top_left, top_right, bottom_left, bottom_right
75
+ ].map { |c| c.is_a?(String) ? c.encode("UTF-8").codepoints.first : c }
76
+ if args.any? { |c| c >= 128 }
77
+ Rurses.curses.wborder_set(
78
+ curses_ref,
79
+ *args.map { |c|
80
+ char = Rurses.curses::WinStruct::CCharT.new
81
+ char[:chars][0] = c
82
+ }
83
+ )
84
+ else
85
+ Rurses.curses.wborder(curses_ref, *args)
86
+ end
87
+ end
88
+
89
+ def move_cursor(x: , y: )
90
+ Rurses.curses.wmove(curses_ref, y, x)
91
+ end
92
+
93
+ def draw_string(content)
94
+ Rurses.curses.waddstr(curses_ref, content)
95
+ end
96
+
97
+ def draw_string_on_a_line(content)
98
+ old_y = cursor_y
99
+ draw_string(content)
100
+ new_y = cursor_y
101
+ move_cursor(x: 0, y: new_y + 1) if new_y == old_y
102
+ end
103
+
104
+ def skip_line
105
+ move_cursor(x: 0, y: cursor_y + 1)
106
+ end
107
+
108
+ def style(*attributes)
109
+ attributes.each do |attribute|
110
+ Rurses.curses.wattron(curses_ref, ATTRIBUTES[attribute])
111
+ end
112
+ yield
113
+ ensure
114
+ attributes.each do |attribute|
115
+ Rurses.curses.wattroff(curses_ref, ATTRIBUTES[attribute])
116
+ end
117
+ end
118
+
119
+ def clear(reset_cursor: true)
120
+ Rurses.curses.wclear(curses_ref)
121
+ move_cursor(x: 0, y: 0) if reset_cursor
122
+ end
123
+
124
+ def refresh_in_memory
125
+ Rurses.curses.wnoutrefresh(curses_ref)
126
+ end
127
+
128
+ def create_subwindow( name: , top_padding: 0, left_padding: 0,
129
+ right_padding: 0, bottom_padding: 0 )
130
+ s = size
131
+ xy = cursor_xy
132
+ subwindows[name] =
133
+ self.class.new(
134
+ curses_ref: Rurses.curses.derwin(
135
+ curses_ref,
136
+ s[:lines] - (top_padding + bottom_padding),
137
+ s[:columns] - (left_padding + right_padding),
138
+ xy[:y] + top_padding,
139
+ xy[:x] + left_padding
140
+ )
141
+ )
142
+ end
143
+
144
+ def subwindow(name)
145
+ subwindows[name]
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,46 @@
1
+ module TerminalNotes
2
+ class SearchField < Widget
3
+ WIDTH = 0.5
4
+ HEIGHT = 5
5
+ POS_X = 0
6
+ POS_Y = 1
7
+ ALIGN = :center
8
+
9
+ def initialize(parent:)
10
+ super(parent: parent, title: "Search",
11
+ width: WIDTH, height: HEIGHT,
12
+ x: POS_X, y: POS_Y,
13
+ align: ALIGN)
14
+
15
+ @text_field = TextField.new(@parent,
16
+ position: @position.dup.moveBy(2, 2),
17
+ width: @width - 4)
18
+
19
+ @has_focus = false
20
+
21
+ draw
22
+ end
23
+
24
+ def text
25
+ @text_field.text
26
+ end
27
+
28
+ def focus
29
+ @has_focus = true
30
+ @text_field.draw
31
+ end
32
+
33
+ def unfocus
34
+ @has_focus = false
35
+ end
36
+
37
+ def on_text_changed &delegate
38
+ @text_field.on_text_changed { |p| delegate.call(p) }
39
+ end
40
+
41
+ def on_key key
42
+ return unless @has_focus
43
+ @text_field.on_key(key)
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,89 @@
1
+ module TerminalNotes
2
+ class TextField
3
+ attr_reader :text
4
+
5
+ DEFAULT_WIDTH = 10
6
+ BREAK_CHARS = " -_+=".split('')
7
+
8
+ def initialize(screen, position: Cursor.new, width: DEFAULT_WIDTH)
9
+ @screen = screen
10
+ @start = position.dup
11
+ @end = @start.dup.moveBy(width, 0)
12
+ @width = width
13
+ @cursor = @start.dup
14
+
15
+ @clear_text = " " * @width
16
+ @text = ""
17
+
18
+ @on_text_changed_delegates = []
19
+ end
20
+
21
+ def on_text_changed &block
22
+ @on_text_changed_delegates << block
23
+ end
24
+
25
+ def notify_text_changed
26
+ @on_text_changed_delegates.each do |delegate|
27
+ delegate.call(@text)
28
+ end
29
+ end
30
+
31
+ def on_key(key)
32
+ old_text = @text
33
+ case key
34
+ when :BACKSPACE
35
+ @text = @text[0...-1]
36
+ @cursor.moveBy(-1) unless @text.size == (@width - 1)
37
+
38
+ when :UP, :DOWN
39
+ when :LEFT
40
+ @cursor.moveBy(-1)
41
+
42
+ when :RIGHT
43
+ @cursor.moveBy(1)
44
+
45
+ when :CTRL_K
46
+ @text = ""
47
+
48
+ when :CTRL_W
49
+ last = @text.size - 2
50
+ while last >= 0
51
+ break if BREAK_CHARS.include? @text[last]
52
+ last -= 1
53
+ end
54
+ @text = @text[0...(last+1)]
55
+
56
+ when :CTRL_E
57
+ @cursor.moveTo(x: @end.x)
58
+
59
+ when :CTRL_A
60
+ @cursor.moveTo(x: @start.x)
61
+
62
+ else
63
+ if !key.nil? && !key.is_a?(Symbol) && @text.size < @width
64
+ @text += key
65
+ @cursor.moveBy(1)
66
+ end
67
+ end
68
+
69
+ # Make sure we don't move cursor too far left
70
+ @cursor.moveTo(x: @start.x) if @cursor.x < @start.x
71
+
72
+ # Make sure we don't move cursor too far right
73
+ max_x = @start.x + @text.size
74
+ max_x -= 1 if @text.size == @width
75
+ @cursor.moveTo(x: max_x) if @cursor.x > max_x
76
+
77
+ draw
78
+
79
+ notify_text_changed if old_text != @text
80
+ end
81
+
82
+ def draw
83
+ @screen.move_cursor(@start.to_hash)
84
+ @screen.draw_string(@text.ljust(@width))
85
+
86
+ @screen.move_cursor(@cursor.to_hash)
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,96 @@
1
+ module TerminalNotes
2
+ class Widget
3
+ attr_reader :window, :width, :height
4
+
5
+ TITLE_PADDING = 4
6
+
7
+ def initialize(parent:, title:, width: 1, height: 1,
8
+ x: 0, y: 0, align: :center, valign: :center,
9
+ border: true)
10
+ @parent = parent
11
+ @title = title || ""
12
+
13
+ @width = width
14
+ @height = height
15
+
16
+ parent_size = @parent.size
17
+
18
+ # Relative layout calculation
19
+ remaining_width = @width
20
+ if @width <= 1
21
+ remaining_width = parent_size[:columns] - x
22
+ @width = (@width * remaining_width).to_i
23
+ end
24
+
25
+ remaining_height = @height
26
+ if @height <= 1
27
+ remaining_height = parent_size[:lines] - y
28
+ @height = (@height * remaining_height).to_i
29
+ end
30
+
31
+ if align == :center
32
+ pos_x = x + (remaining_width - @width) / 2
33
+ @title_x = (@width - @title.size) / 2
34
+ elsif align == :right
35
+ pos_x = (remaining_width - @width)
36
+ @title_x = @width - @title.size - TITLE_PADDING
37
+ else
38
+ pos_x = x # do nothing
39
+ @title_x = TITLE_PADDING
40
+ end
41
+
42
+ if valign == :center
43
+ pos_y = y + (remaining_height - @height) / 2
44
+ elsif valign == :bottom
45
+ pos_y = y + (remaining_height - @height)
46
+ else
47
+ pos_y = y # do nothing
48
+ end
49
+
50
+ @position = Cursor.new(x: pos_x, y: pos_y)
51
+
52
+ @window = Rurses::Window.new(
53
+ lines: @height, columns: @width,
54
+ x: @position.x, y: @position.y)
55
+
56
+ @has_border = border
57
+
58
+ # @window.refresh_in_memory
59
+ end
60
+
61
+ def redraw
62
+ @window.clear
63
+ draw
64
+ end
65
+
66
+ def draw
67
+ old_cursor = @window.cursor_xy
68
+
69
+ # Draw title
70
+ if @has_border || !@title.empty?
71
+ @window.draw_border
72
+ @window.move_cursor(x: @title_x, y: 0)
73
+ @window.draw_string(" #{@title} ")
74
+ end
75
+
76
+ yield if block_given?
77
+
78
+ @window.move_cursor(old_cursor)
79
+ @window.refresh_in_memory
80
+ end
81
+
82
+ def focus
83
+ end
84
+
85
+ def resize
86
+ calculate_and_draw
87
+ end
88
+
89
+ private
90
+
91
+ def calculate_and_draw(new_window: false)
92
+ @window.clear
93
+ @window.refresh_in_memory
94
+ end
95
+ end
96
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: terminal-notes
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Vidur Murali
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2020-08-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: ffi-ncurses
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.4.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.4.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: fuzzy_match
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.1.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 13.0.1
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 13.0.1
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 3.9.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 3.9.0
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubygems-tasks
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: 0.2.5
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: 0.2.5
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.19.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.19.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: yard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.9.25
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.9.25
111
+ description:
112
+ email:
113
+ - vidur@monkeychai.com
114
+ executables:
115
+ - terminal-notes
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - README.md
120
+ - bin/terminal-notes
121
+ - lib/terminal-notes.rb
122
+ - lib/terminal-notes/cursor.rb
123
+ - lib/terminal-notes/file_list.rb
124
+ - lib/terminal-notes/info_bar.rb
125
+ - lib/terminal-notes/rurses.rb
126
+ - lib/terminal-notes/rurses/panel_stack.rb
127
+ - lib/terminal-notes/rurses/version.rb
128
+ - lib/terminal-notes/rurses/window.rb
129
+ - lib/terminal-notes/search_field.rb
130
+ - lib/terminal-notes/text_field.rb
131
+ - lib/terminal-notes/widget.rb
132
+ homepage: https://github.com/vyder/terminal-notes
133
+ licenses:
134
+ - MIT
135
+ metadata:
136
+ documentation_uri: https://vyder.github.io/terminal-notes
137
+ post_install_message:
138
+ rdoc_options: []
139
+ require_paths:
140
+ - lib
141
+ required_ruby_version: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ required_rubygems_version: !ruby/object:Gem::Requirement
147
+ requirements:
148
+ - - ">="
149
+ - !ruby/object:Gem::Version
150
+ version: '0'
151
+ requirements: []
152
+ rubygems_version: 3.1.4
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: Searchable notes in your terminal! What's not to like?
156
+ test_files: []