feat: Introduce colored text windows and enhanced positioning

This commit introduces a significant enhancement to the DynamicCursesInput gem, enabling the creation of colored text windows within the terminal. The new `DynamicCursesInput::ColorWindow` class provides methods to add colored text to the window and handle user input.

Notable changes in this commit include:
- Implementation of colored windows with customizable color schemes and text content.
- Automatic centering of text within the window when 'x' is set to 'center.'
- Fine-tuning of centered positioning to shift back by 12 cells for improved visual layout.
- Removal of Fiber reliance and adoption of instance methods to resolve positioning issues.
- Compatibility fixes for different terminal environments to ensure consistent behavior.

Additionally, bug fixes have been made to resolve issues related to text misalignment and window positioning.

This feature-rich update aims to improve the user experience and offer better control over window positioning and color schemes. We hope these enhancements will be beneficial to our users. Feedback and suggestions are always welcome!
This commit is contained in:
VetheonGames 2023-07-26 16:31:20 -06:00
parent 5bf14c982a
commit a27f41a601
9 changed files with 226 additions and 58 deletions

1
.gitignore vendored
View File

@ -9,3 +9,4 @@
# rspec failure tracking
.rspec_status
buildgem.sh

View File

@ -1,5 +1,30 @@
## [Unreleased]
## [Release]
## [0.1.0] - 2023-06-07
## [1.0.0] - 2023-06-07
- Initial release
## [Release]
## [1.1.0] - 2023-07-26
- Features and Enhancements:
- Added support for printing colored windows with customized positions and color schemes.
- Introduced the DynamicCursesInput::ColorWindow class, which allows creating colored text windows within the terminal.
- The ColorWindow class provides methods to add colored text to the window and handle user input.
- Implemented automatic centering of text within the window when x is set to 'center'.
- Adjusted the centered position to shift back by 12 cells for better visual layout when necessary.
- Removed reliance on Fibers and replaced it with instance methods to resolve issues with positioning.
- Handled compatibility issues with different terminal environments to ensure consistent behavior.
- Refactored the code to eliminate unnecessary checks for IRB, enabling smooth execution in various contexts.
- Improved the debug log functionality for easier debugging and troubleshooting.
- Bug Fixes:
- Fixed the issue causing text to be misaligned or misplaced in certain terminal environments.
- Resolved a bug where the window position was not being updated correctly in some cases.
- Other Changes:
- Removed redundant and unused code snippets to improve code cleanliness and maintainability.

10
Gemfile
View File

@ -1,12 +1,14 @@
# frozen_string_literal: true
source "https://rubygems.org"
source 'https://rubygems.org'
# Specify your gem's dependencies in dynamic_curses_input.gemspec
gemspec
gem "rake"
gem 'rake'
gem "rubocop"
gem 'rubocop'
gem "curses"
gem 'curses'
gem 'reline'

View File

@ -1,44 +1,38 @@
PATH
remote: .
specs:
dynamic_curses_input (1.0.0)
dynamic_curses_input (1.0.1.1)
curses
reline
GEM
remote: https://rubygems.org/
specs:
ast (2.4.2)
curses (1.4.4)
diff-lcs (1.5.0)
io-console (0.6.0)
json (2.6.3)
language_server-protocol (3.17.0.3)
parallel (1.23.0)
parser (3.2.2.1)
parser (3.2.2.3)
ast (~> 2.4.1)
racc
racc (1.7.1)
rainbow (3.1.1)
rake (13.0.6)
regexp_parser (2.8.0)
regexp_parser (2.8.1)
reline (0.3.6)
io-console (~> 0.5)
rexml (3.2.5)
rspec (3.12.0)
rspec-core (~> 3.12.0)
rspec-expectations (~> 3.12.0)
rspec-mocks (~> 3.12.0)
rspec-core (3.12.2)
rspec-support (~> 3.12.0)
rspec-expectations (3.12.3)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-mocks (3.12.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.12.0)
rspec-support (3.12.0)
rubocop (1.52.0)
rubocop (1.55.0)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.2.0.0)
parser (>= 3.2.2.3)
rainbow (>= 2.2.2, < 4.0)
regexp_parser (>= 1.8, < 3.0)
rexml (>= 3.2.5, < 4.0)
rubocop-ast (>= 1.28.0, < 2.0)
rubocop-ast (>= 1.28.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 3.0)
rubocop-ast (1.29.0)
@ -53,8 +47,8 @@ DEPENDENCIES
curses
dynamic_curses_input!
rake
rspec
reline
rubocop
BUNDLED WITH
2.4.13
2.4.16

View File

@ -1,14 +1,14 @@
# frozen_string_literal: true
require_relative "lib/dynamic_curses_input/version"
require_relative 'lib/dynamic_curses_input/version'
Gem::Specification.new do |spec|
spec.name = "dynamic_curses_input"
spec.name = 'dynamic_curses_input'
spec.version = DynamicCursesInput::VERSION
spec.authors = ["VetheonGames"]
spec.email = ["vetheon@pixelatedstudios.net"]
spec.authors = ['VetheonGames']
spec.email = ['vetheon@pixelatedstudios.net']
spec.summary = "A simple library for making Curses TUI input more dynamic and user-friendly"
spec.summary = 'A simple library for making Curses TUI input more dynamic and user-friendly'
spec.description = "Dynamic Curses Input is a highly simple, yet powerful gem that allows simple implementation of
dynamic typing in curses TUI menus built in Ruby. For example, one can't simply use their arrow
keys to navigate and edit inputs in Cursese TUI menus without adding a bunch of extra code to your
@ -17,23 +17,23 @@ Gem::Specification.new do |spec|
allowing the special keys to work as the average user would expect.
IE: When you press the left arrow key, the cursor moves to the left and allows you to delete a
character you entered that isn't the last character you entered."
spec.homepage = "https://github.com/Pixelated-Studios/dynamic_curses_input"
spec.license = "MIT"
spec.required_ruby_version = "3.2.2"
spec.homepage = 'https://github.com/Pixelated-Studios/dynamic_curses_input'
spec.license = 'MIT'
spec.required_ruby_version = '3.2.2'
spec.metadata["homepage_uri"] = spec.homepage
spec.metadata["source_code_uri"] = "https://github.com/Pixelated-Studios/dynamic_curses_input"
spec.metadata["changelog_uri"] = "https://github.com/Pixelated-Studios/dynamic_curses_input/blob/main/CHANGELOG.md"
spec.metadata['homepage_uri'] = spec.homepage
spec.metadata['source_code_uri'] = 'https://github.com/Pixelated-Studios/dynamic_curses_input'
spec.metadata['changelog_uri'] = 'https://github.com/Pixelated-Studios/dynamic_curses_input/blob/main/CHANGELOG.md'
# Specify which files should be added to the gem when it is released.
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
spec.files = Dir.chdir(__dir__) do
`git ls-files -z`.split("\x0").reject do |f|
f.start_with?("spec", ".rspec") || (File.expand_path(f) == __FILE__)
end
end
spec.require_paths = ["lib"]
spec.files = Dir.glob('{bin,lib,sig}/**/*') + Dir.glob('*').reject { |f| f.start_with?('spec', '.rspec', 'dynamic_curses_input.gemspec') }
spec.files << 'LICENSE.txt'
spec.files << 'README.md'
spec.files << 'dynamic_curses_input.gemspec'
spec.add_dependency "curses"
spec.add_development_dependency "rubocop"
spec.require_paths = ['lib']
spec.add_dependency 'curses'
spec.add_dependency 'reline'
spec.add_development_dependency 'rubocop'
end

View File

@ -2,8 +2,10 @@
# lib/dynamic_curses_input.rb
require_relative "dynamic_curses_input/version"
require_relative "dynamic_curses_input/input_handler"
require 'readline' # Add the Readline module
require_relative 'dynamic_curses_input/version'
require_relative 'dynamic_curses_input/input_handler'
require_relative 'dynamic_curses_input/color_window'
# The module entrypoint for our Gem
module DynamicCursesInput
@ -13,12 +15,74 @@ module DynamicCursesInput
InputHandler.catch_input(echo)
end
def self.ask_question(question, echo)
def self.ask_question(color = 'white', question, x: 'center', input: true, echo: nil)
Curses.clear
Curses.setpos(1, 0)
Curses.addstr(question)
ColorWindow.add_color_window(color, question, y:, x:, input:, echo:)
end
def self.print_color_window(color, text, y_value: nil, x: 'center', input: nil, echo: true)
case x
when 'center'
terminal_size = `stty size`.split.map(&:to_i)
y_value = terminal_size[0] / 2
x_value = terminal_size[1] / 2 - text.length / 2 # Adjust x-coordinate to center the window
x_value -= 12 if x_value > 1
# the above line shifts the X value of the cell coords back by 12 cells if we are trying to center the window
# we have to do this because math gets kind of approximate when we convert pixel ratios to character cell coords
when 'left'
y_value = Curses.lines / 2
x_value = 0
when 'right'
y_value = Curses.lines / 2
x_value = Curses.cols - text.length
when 'left_center'
y_value = Curses.lines / 4
x_value = 0
when 'right_center'
y_value = Curses.lines / 4
x_value = Curses.cols - text.length
else
y_value, x_value = x.split('px').map(&:to_i)
end
# Initialize curses and get the terminal size
Curses.init_screen
Curses.start_color
Curses.refresh
catch_input(echo)
# Set up Readline for proper terminal settings
setup_readline
ColorWindow.new(echo, x_value, y_value).add_color_window(color, text, x_value, y_value, input:, echo:)
end
class << self
private
def process_print_color_window_args(args)
case args.size
when 2
['white', args[0], args[1], 'center', nil, true]
when 3
['white', args[0], args[1], args[2], nil, true]
when 4
['white', args[0], args[1], args[2], args[3], true]
when 5
[args[0], args[1], args[2], args[3], args[4]]
else
raise ArgumentError, 'print_color_window accepts 2 to 5 arguments: color, text, [position], [input], [echo]'
end
end
end
def self.setup_readline
# Set up Readline for proper terminal settings
Readline.emacs_editing_mode
# the above line sets Readline to emacs_editing_mode so that terminals behave like they're supposed to
Readline.completion_append_character = ' '
# we remove Readlines thing where it adds a space to the end of tab completes because it breaks the Curses cursor
Readline.completion_proc = proc { |_s| [] }
# here we basically are disabling tab completion all together. That's because for some reason it breaks the cursor
end
end

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
# lib/dynamic_curses_input/color_window.rb
require 'curses'
require_relative 'input_handler'
module DynamicCursesInput
# Class for creating a colored window
class ColorWindow
# Initialize instance variables and setup curses
def initialize(echo, x, y)
@echo = echo # Determines whether input should be echoed to the screen
setup_curses_color # Setup curses
@x = x
@y = y
# Define color pairs
Curses.init_pair(1, Curses::COLOR_BLACK, Curses::COLOR_BLACK)
Curses.init_pair(2, Curses::COLOR_BLUE, Curses::COLOR_BLACK)
Curses.init_pair(3, Curses::COLOR_GREEN, Curses::COLOR_BLACK)
Curses.init_pair(4, Curses::COLOR_CYAN, Curses::COLOR_BLACK)
Curses.init_pair(5, Curses::COLOR_RED, Curses::COLOR_BLACK)
Curses.init_pair(6, Curses::COLOR_MAGENTA, Curses::COLOR_BLACK)
Curses.init_pair(7, Curses::COLOR_YELLOW, Curses::COLOR_BLACK) # Brown is usually represented as yellow
Curses.init_pair(8, Curses::COLOR_WHITE, Curses::COLOR_BLACK)
end
# Method that adds colored text to the window
def add_color_window(color, text, x, y, input: nil, echo: true)
# Map color names to color pair numbers
color_map = {
'black' => 1,
'blue' => 2,
'green' => 3,
'cyan' => 4,
'red' => 5,
'magenta' => 6,
'brown' => 7, # Brown is usually represented as yellow in terminal colors
'white' => 8
}
# Get the color pair number for the specified color
color_pair = color_map[color.downcase]
# Set the cursor position if both x and y are specified
if x && y
set_position(y, x)
elsif x.nil? && y.nil?
# If both x and y are not specified, raise an ArgumentError
raise ArgumentError, 'Both x and y coordinates must be specified for printing the color window.'
end
# Print the text in the specified color
Curses.attron(Curses.color_pair(color_pair))
Curses.addstr(text)
Curses.attroff(Curses.color_pair(color_pair))
# If an input is specified, take input from the user
InputHandler.catch_input(echo) if input
Curses.refresh
end
private
# Setup curses
def setup_curses_color
Curses.init_screen
Curses.start_color
end
# Set cursor position manually on the X and Y axis
def set_position(y, x)
Curses.setpos(y, x)
end
end
end

View File

@ -2,7 +2,7 @@
# lib/dynamic_curses_input/input_handler.rb
require "curses"
require 'curses'
module DynamicCursesInput
# our main class for handling input
@ -15,7 +15,7 @@ module DynamicCursesInput
# Initialize instance variables and setup curses
def initialize(echo)
@echo = echo # Determines whether input should be echoed to the screen
@input = "" # Stores the input string
@input = '' # Stores the input string
@cursor_pos = 0 # Stores the current cursor position
@initial_y = Curses.stdscr.cury # Stores the initial y-coordinate of the cursor
@initial_x = Curses.stdscr.curx # Stores the initial x-coordinate of the cursor
@ -84,7 +84,7 @@ module DynamicCursesInput
# Redraw the input string
def redraw_input
Curses.setpos(@initial_y, @initial_x) # Move cursor to initial position
Curses.addstr(" " * (Curses.cols - @initial_x)) # Clear line
Curses.addstr(' ' * (Curses.cols - @initial_x)) # Clear line
Curses.setpos(@initial_y, @initial_x) # Move cursor to initial position
Curses.addstr(@input) if @echo # Draw input string if @echo is true
Curses.setpos(@initial_y, @initial_x + @cursor_pos) # Move cursor to current position
@ -102,6 +102,11 @@ module DynamicCursesInput
def self.right(cursor_pos, length)
cursor_pos == length ? cursor_pos : cursor_pos + 1
end
# Set cursor position
def self.set_position(y, x)
Curses.setpos(y, x)
end
end
# Class for deleting characters

View File

@ -1,5 +1,5 @@
# frozen_string_literal: true
module DynamicCursesInput
VERSION = "1.0.0"
VERSION = '1.1.0'
end