terminal-notes 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []