2 changed files with 240 additions and 1 deletions
@ -0,0 +1,238 @@ |
|||||||
|
#!/usr/bin/env tclsh |
||||||
|
|
||||||
|
# Copyright (C) 2021, Maxim Lihachev, <envrm@yandex.ru> |
||||||
|
# |
||||||
|
# This program is free software: you can redistribute it and/or modify it |
||||||
|
# under the terms of the GNU General Public License as published by the Free |
||||||
|
# Software Foundation, version 3. |
||||||
|
# |
||||||
|
# This program is distributed in the hope that it will be useful, but WITHOUT |
||||||
|
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or |
||||||
|
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for |
||||||
|
# more details. |
||||||
|
# |
||||||
|
# You should have received a copy of the GNU General Public License along with |
||||||
|
# this program. If not, see <https://www.gnu.org/licenses/>. |
||||||
|
|
||||||
|
# |
||||||
|
# Simple Bingo-style TODO visualizer. |
||||||
|
# |
||||||
|
# Input: |
||||||
|
# |
||||||
|
# todo.md: |
||||||
|
# |
||||||
|
# - [ ] A |
||||||
|
# - [x] B |
||||||
|
# - [x] C |
||||||
|
# - [ ] … |
||||||
|
# - [ ] V |
||||||
|
# |
||||||
|
# GUI: |
||||||
|
# |
||||||
|
# ┌───────────────────────┐ |
||||||
|
# ├───┬───┬───┬───┬───┬───┤ |
||||||
|
# │ A │ B │ C │ D │ E │ F │ |
||||||
|
# ├───┼───┼───┼───┼───┼───┤ |
||||||
|
# │ A │ B │ C │ D │ E │ F │ |
||||||
|
# ├───┼───┼───┼───┼───┼───┤ |
||||||
|
# │ G │ H │ I │ J │ K │ L │ |
||||||
|
# ├───┼───┼───┼───┼───┼───┤ |
||||||
|
# │ M │ N │ O │ P │ Q │ R │ |
||||||
|
# ├───┴─┬─┴───┼───┴─┬─┴───┤ |
||||||
|
# │ S │ T │ U │ V │ |
||||||
|
# ├─────┴─────┴─────┴─────┤ |
||||||
|
# │ 10/22 [45%] │ |
||||||
|
# └───────────────────────┘ |
||||||
|
# |
||||||
|
|
||||||
|
# |
||||||
|
# TODO: работа с повторяющимися строками |
||||||
|
# |
||||||
|
|
||||||
|
package require fileutil |
||||||
|
|
||||||
|
proc help {} { |
||||||
|
puts "USAGE: [file tail $::argv0] <todo.md>" |
||||||
|
} |
||||||
|
|
||||||
|
namespace eval todo { |
||||||
|
set file {} |
||||||
|
set data {} |
||||||
|
set list {} |
||||||
|
|
||||||
|
set status {} |
||||||
|
|
||||||
|
array set format { |
||||||
|
marker {^\s*-\s+\[[ xX]\]\s*} |
||||||
|
done {^\s*-\s+\[[xX]\]\s*} |
||||||
|
todo {^\s*-\s+\[[ ]\]\s*} |
||||||
|
} |
||||||
|
|
||||||
|
proc sanitize_string {str} { |
||||||
|
string map { |
||||||
|
{*} {\*} {+} {\+} {[} {\[} {]} {\]} {.} {\.} |
||||||
|
{(} {\(} {)} {\)} {\{} {\\{} {\}} {\\}} |
||||||
|
} $str |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
proc load {filename} { |
||||||
|
todo::update [split [fileutil::cat [set ::todo::file $filename]] "\n"] |
||||||
|
todo::update_status |
||||||
|
} |
||||||
|
|
||||||
|
proc update {data} { |
||||||
|
set ::todo::data $data |
||||||
|
} |
||||||
|
|
||||||
|
proc update_list {} { |
||||||
|
set ::todo::list [lsearch -all -inline -regexp $::todo::data {^[\s]*-[\s]\[[ xX]\]\s.*$}] |
||||||
|
} |
||||||
|
|
||||||
|
proc update_status {} { |
||||||
|
::todo::update_list |
||||||
|
|
||||||
|
set total_items [llength $::todo::list] |
||||||
|
set done_items [llength [regexp -inline -all -nocase {\[[xX]\]} $::todo::list]] |
||||||
|
|
||||||
|
set percents [expr {$done_items * 100 / $total_items}] |
||||||
|
|
||||||
|
set ::todo::status "$done_items/$total_items \[${percents}%\]" |
||||||
|
} |
||||||
|
|
||||||
|
proc update_file {} { |
||||||
|
fileutil::writeFile $::todo::file [join $::todo::data "\n"] |
||||||
|
} |
||||||
|
|
||||||
|
proc update_item {item} { |
||||||
|
set item_pattern $::todo::format(marker)[::todo::sanitize_string [lindex $item end]] |
||||||
|
set todo_string [lsearch -inline -regexp $::todo::data $item_pattern] |
||||||
|
set todo_index [lsearch -exact $::todo::data $todo_string] |
||||||
|
set updated_todo [::todo::toggle_marker $todo_string] |
||||||
|
|
||||||
|
::todo::update_list |
||||||
|
::todo::update [lreplace $::todo::data $todo_index $todo_index $updated_todo] |
||||||
|
::todo::update_status |
||||||
|
::todo::update_file |
||||||
|
} |
||||||
|
|
||||||
|
# Toggle todo markers: |
||||||
|
# "- [ ]" -> "- [x]" |
||||||
|
# "- [x]" -> "- [ ]" |
||||||
|
proc toggle_marker {str} { |
||||||
|
string map {{- [ ]} {- [x]} {- [x]} {- [ ]} {- [X]} {- [ ]}} $str |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
namespace eval gui { |
||||||
|
proc set_theme {} { |
||||||
|
ttk::style theme use clam |
||||||
|
|
||||||
|
set layout [ttk::style layout TButton] |
||||||
|
set tail [lindex $layout end] |
||||||
|
|
||||||
|
ttk::style layout Button.TODO [lreplace $layout end end $tail] |
||||||
|
ttk::style configure Button.TODO {*}[ttk::style configure TButton] -background LightBlue -wraplength 150 |
||||||
|
ttk::style map Button.TODO {*}[ttk::style map TButton] -background {active LightSteelBlue} |
||||||
|
|
||||||
|
ttk::style layout Button.DONE [lreplace $layout end end $tail] |
||||||
|
ttk::style configure Button.DONE {*}[ttk::style configure TButton] -background GreenYellow -wraplength 150 |
||||||
|
ttk::style map Button.DONE {*}[ttk::style map TButton] -background {active LawnGreen} |
||||||
|
} |
||||||
|
|
||||||
|
proc set_button_status {button status} { |
||||||
|
set status [string toupper $status] |
||||||
|
|
||||||
|
$button configure -style Button.$status |
||||||
|
|
||||||
|
if {$status == "TODO"} { |
||||||
|
set ::gui::state($button) 0 |
||||||
|
} else { |
||||||
|
set ::gui::state($button) 1 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
proc toggle_button_status {button} { |
||||||
|
if {$::gui::state($button)} { |
||||||
|
::gui::set_button_status $button todo |
||||||
|
} else { |
||||||
|
::gui::set_button_status $button done |
||||||
|
} |
||||||
|
|
||||||
|
::todo::update_item [lsearch -inline [$button configure] {-text*}] |
||||||
|
} |
||||||
|
|
||||||
|
proc show {} { |
||||||
|
::gui::set_theme |
||||||
|
|
||||||
|
set columns [expr {ceil(sqrt([llength $::todo::list]))}] |
||||||
|
|
||||||
|
lassign {0 0 0} current_column current_frame current_todo_item |
||||||
|
|
||||||
|
foreach todo_item $::todo::list { |
||||||
|
if {$current_column == 0} { |
||||||
|
pack [set f [frame .[incr current_frame]]] -expand on -fill both -side top |
||||||
|
} |
||||||
|
|
||||||
|
::gui::draw_todo_button "${f}.todo-[incr current_todo_item]" $todo_item |
||||||
|
|
||||||
|
if {$current_column == $columns} { |
||||||
|
set current_column 0 |
||||||
|
} else { |
||||||
|
incr current_column |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
::gui::draw_statusbar |
||||||
|
::gui::bind_hotkeys |
||||||
|
} |
||||||
|
|
||||||
|
proc draw_todo_button {widget item} { |
||||||
|
# Remove todo markers and useless spaces |
||||||
|
regsub -expanded $::todo::format(marker) $item {} title |
||||||
|
|
||||||
|
pack [ttk::button $widget -text $title -width 1 \ |
||||||
|
-command "::gui::toggle_button_status $widget"] \ |
||||||
|
-expand on -side left -fill both |
||||||
|
|
||||||
|
tooltip::tooltip $widget $title |
||||||
|
|
||||||
|
if {[regexp -expanded $::todo::format(todo) $item]} { |
||||||
|
::gui::set_button_status $widget todo |
||||||
|
} else { |
||||||
|
::gui::set_button_status $widget done |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
proc draw_statusbar {} { |
||||||
|
pack [frame .statusbar] -fill x |
||||||
|
pack [label .statusbar.status -font {Serif 10} -textvariable ::todo::status -padx 3 -anchor e] -fill x |
||||||
|
} |
||||||
|
|
||||||
|
proc bind_hotkeys {} { |
||||||
|
bind . <Control-q> exit |
||||||
|
bind . <q> exit |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
# ------------------------------------------------------------------------------ |
||||||
|
|
||||||
|
if {$argc == 0} { |
||||||
|
help |
||||||
|
exit 1 |
||||||
|
} else { |
||||||
|
set filename [lindex $argv 0] |
||||||
|
|
||||||
|
if {![file exists $filename] || ![file writable $filename]} { |
||||||
|
puts stderr "ERROR: File $filename does not exist or it is not writable." |
||||||
|
exit 1 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
package require Tk |
||||||
|
package require tooltip |
||||||
|
|
||||||
|
todo::load $filename |
||||||
|
|
||||||
|
gui::show |
||||||
|
|
Loading…
Reference in new issue