From 1ca023c94b2ecee7ba7541e0986aac04e6dee551 Mon Sep 17 00:00:00 2001 From: Maxim Likhachev Date: Fri, 12 Feb 2021 17:27:55 +0300 Subject: [PATCH] ++todo-bingo --- README.md | 3 +- scripts/todo-bingo | 238 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+), 1 deletion(-) create mode 100755 scripts/todo-bingo diff --git a/README.md b/README.md index bb791f8..03c4dbb 100644 --- a/README.md +++ b/README.md @@ -60,11 +60,12 @@ will create the symbolic link for each script in the \ directory. - [photosort](scripts/photosort) renames the image files by exif data. - [th](scripts/th) makes an image thumbnail. -## Notes +## Notes, Todo Tools - [dates](scripts/dates) + conky or similar software remind of upcoming events (birthdays, etc.). - [mem](scripts/mem) stores the shorts notes in the file. - [pomodoro](scripts/pomodoro) tracks the working time. +- [todo-bingo](scripts/todo-bingo) provides a simple GUI to visualize TODO lists. ## Network diff --git a/scripts/todo-bingo b/scripts/todo-bingo new file mode 100755 index 0000000..4ee5261 --- /dev/null +++ b/scripts/todo-bingo @@ -0,0 +1,238 @@ +#!/usr/bin/env tclsh + +# Copyright (C) 2021, Maxim Lihachev, +# +# 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 . + +# +# 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] " +} + +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 . exit + bind . 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 +